refactor(e2e): organize fixtures by feature
This commit is contained in:
@@ -1,112 +0,0 @@
|
|||||||
# 聊天数量限制测试
|
|
||||||
|
|
||||||
## 功能点
|
|
||||||
|
|
||||||
验证游客在聊天页连续发送消息后,是否会在限定次数内触发聊天数量限制;触发后是否能够继续完成登录、订阅、支付和返回聊天页的完整链路。
|
|
||||||
|
|
||||||
## 测试目标
|
|
||||||
|
|
||||||
1. 游客进入聊天页后,连续聊天最多 `51` 条。
|
|
||||||
2. 若在 `51` 条以内触发聊天限制,则继续验证后续跳转和支付链路。
|
|
||||||
3. 若发送到第 `51` 条后仍未出现目标受限界面,则判定本功能点测试失败。
|
|
||||||
|
|
||||||
## 测试网址
|
|
||||||
|
|
||||||
- 统一测试网址请查看:[test-site.md](./test-site.md)
|
|
||||||
|
|
||||||
## 目标受限界面
|
|
||||||
|
|
||||||
出现以下现象时,视为命中目标界面:
|
|
||||||
|
|
||||||
1. 聊天输入框不可见或不可继续输入。
|
|
||||||
2. 页面出现跳转按钮,用于继续前往登录或订阅流程。
|
|
||||||
|
|
||||||
## 测试数据
|
|
||||||
|
|
||||||
### 聊天数据
|
|
||||||
|
|
||||||
- 消息发送上限:`51`
|
|
||||||
- 建议消息模板:
|
|
||||||
- `limit test 01`
|
|
||||||
- `limit test 02`
|
|
||||||
- `limit test 03`
|
|
||||||
- ...
|
|
||||||
- `limit test 51`
|
|
||||||
|
|
||||||
### 登录流程引用
|
|
||||||
|
|
||||||
- 登录相关操作请参考:[email-login-from-other-options.md](./email-login-from-other-options.md) 中的“阶段三:进入邮箱登录方式”
|
|
||||||
|
|
||||||
## 操作流程
|
|
||||||
|
|
||||||
### 阶段一:进入聊天页并验证数量限制
|
|
||||||
|
|
||||||
1. 打开统一测试网址(见 [test-site.md](./test-site.md))
|
|
||||||
2. 在首页点击 `Skip`
|
|
||||||
3. 进入聊天界面
|
|
||||||
4. 以顺序消息数据持续发送聊天内容,从 `limit test 01` 开始,最多发送到 `limit test 51`
|
|
||||||
5. 在发送过程中持续观察页面状态
|
|
||||||
|
|
||||||
### 阶段二:数量限制判定
|
|
||||||
|
|
||||||
1. 若在第 `1` 至第 `51` 条消息之间出现目标受限界面:
|
|
||||||
- 继续执行后续登录与订阅流程
|
|
||||||
2. 若发送完第 `51` 条消息后仍未出现目标受限界面:
|
|
||||||
- 直接判定该功能点测试失败
|
|
||||||
- 停止后续登录、订阅、支付步骤
|
|
||||||
|
|
||||||
## 触发限制后的后续流程
|
|
||||||
|
|
||||||
### 阶段三:登录流程
|
|
||||||
|
|
||||||
1. 点击受限界面中的跳转按钮
|
|
||||||
2. 进入登录界面
|
|
||||||
3. 登录相关操作不在本文件内重复展开
|
|
||||||
4. 直接参考 [email-login-from-other-options.md](./email-login-from-other-options.md) 中的“阶段三:进入邮箱登录方式”
|
|
||||||
5. 登录成功后继续执行后续订阅流程
|
|
||||||
|
|
||||||
### 阶段四:订阅支付与回跳验证
|
|
||||||
|
|
||||||
1. 登录成功后,确认跳转到订阅界面
|
|
||||||
2. 点击 `Pay And Activate` 按钮
|
|
||||||
3. 等待支付成功
|
|
||||||
4. 点击 `OK` 按钮
|
|
||||||
5. 等待页面跳转回聊天界面
|
|
||||||
|
|
||||||
## 预期结果
|
|
||||||
|
|
||||||
### 数量限制预期
|
|
||||||
|
|
||||||
1. 游客在连续聊天过程中,会在第 `51` 条消息以内命中聊天数量限制。
|
|
||||||
2. 命中限制后,聊天输入框不可继续使用。
|
|
||||||
3. 页面出现用于继续流程的跳转按钮。
|
|
||||||
|
|
||||||
### 登录与支付链路预期
|
|
||||||
|
|
||||||
1. 点击跳转按钮后,可进入登录界面。
|
|
||||||
2. 按照邮箱登录测试流程完成登录后,可进入订阅界面。
|
|
||||||
3. 订阅和 Stripe 流程可正常操作。
|
|
||||||
4. 支付成功后,点击 `OK` 可返回聊天界面。
|
|
||||||
|
|
||||||
## 失败判定
|
|
||||||
|
|
||||||
满足以下任一条件即判定本功能点失败:
|
|
||||||
|
|
||||||
1. 发送到第 `51` 条消息后,仍未出现目标受限界面。
|
|
||||||
2. 触发限制后,跳转按钮不存在或无法点击。
|
|
||||||
3. 无法进入登录界面。
|
|
||||||
4. 按照邮箱登录测试流程执行后登录失败。
|
|
||||||
5. 无法进入订阅页。
|
|
||||||
6. Stripe 流程无法继续。
|
|
||||||
7. 支付成功后无法返回聊天页。
|
|
||||||
|
|
||||||
## 执行备注
|
|
||||||
|
|
||||||
1. 若执行过程中出现验证码,需要人工确认后再继续。
|
|
||||||
2. 若支付步骤会产生真实外部副作用,应在执行支付提交前再次确认。
|
|
||||||
3. 建议在正式执行时逐条记录:
|
|
||||||
- 实际发送到第几条时触发限制
|
|
||||||
- 跳转按钮文案
|
|
||||||
- 登录流程参考文件及登录跳转结果
|
|
||||||
- 支付结果
|
|
||||||
- 最终是否成功返回聊天页
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
# Other Sign In Options 邮箱登录测试
|
|
||||||
|
|
||||||
## 功能点
|
|
||||||
|
|
||||||
验证用户从 `splash` 页进入聊天页后,通过顶部 `Sign up to unlock more features` 进入登录页,再通过 `Other Sign In Options` 选择邮箱登录,最终返回聊天界面的流程是否正常。
|
|
||||||
|
|
||||||
## 测试目标
|
|
||||||
|
|
||||||
1. 从 `splash` 页点击 `Skip` 进入聊天界面。
|
|
||||||
2. 从聊天页顶部入口进入登录界面。
|
|
||||||
3. 点击 `Other Sign In Options` 弹出选择弹窗。
|
|
||||||
4. 点击 `Email Sign In` 后等待界面刷新到邮箱登录表单。
|
|
||||||
5. 点击 `Login` 按钮并等待跳转回聊天界面。
|
|
||||||
|
|
||||||
## 测试网址
|
|
||||||
|
|
||||||
- 统一测试网址请查看:[test-site.md](./test-site.md)
|
|
||||||
|
|
||||||
## 测试数据
|
|
||||||
|
|
||||||
- 测试网址引用:见 [test-site.md](./test-site.md)
|
|
||||||
|
|
||||||
## 操作流程
|
|
||||||
|
|
||||||
### 阶段一:进入聊天页
|
|
||||||
|
|
||||||
1. 打开统一测试网址(见 [test-site.md](./test-site.md))
|
|
||||||
2. 进入 `splash` 界面
|
|
||||||
3. 点击 `Skip` 按钮
|
|
||||||
4. 等待页面跳转到聊天界面
|
|
||||||
|
|
||||||
### 阶段二:进入登录页
|
|
||||||
|
|
||||||
1. 在聊天页顶部点击 `Sign up to unlock more features`
|
|
||||||
2. 等待页面跳转到登录界面
|
|
||||||
|
|
||||||
### 阶段三:进入邮箱登录方式
|
|
||||||
|
|
||||||
1. 在登录界面点击 `Other Sign In Options`
|
|
||||||
2. 等待选择弹窗出现
|
|
||||||
3. 在弹窗中点击 `Email Sign In`
|
|
||||||
4. 等待界面刷新为邮箱登录界面
|
|
||||||
|
|
||||||
### 阶段四:提交登录
|
|
||||||
|
|
||||||
1. 点击 `Login` 按钮
|
|
||||||
2. 等待登录完成
|
|
||||||
|
|
||||||
### 阶段五:验证跳转结果
|
|
||||||
|
|
||||||
1. 确认页面从登录流程返回站内页面
|
|
||||||
2. 等待页面跳转回聊天界面
|
|
||||||
3. 确认聊天界面展示正常
|
|
||||||
|
|
||||||
## 预期结果
|
|
||||||
|
|
||||||
1. 点击 `Skip` 后可正常从 `splash` 页进入聊天界面。
|
|
||||||
2. 点击顶部 `Sign up to unlock more features` 后可进入登录界面。
|
|
||||||
3. 点击 `Other Sign In Options` 后可弹出选择弹窗。
|
|
||||||
4. 点击 `Email Sign In` 后可切换到邮箱登录界面。
|
|
||||||
5. 点击 `Login` 后可成功完成登录。
|
|
||||||
6. 登录成功后可正常跳转回聊天界面。
|
|
||||||
|
|
||||||
## 失败判定
|
|
||||||
|
|
||||||
满足以下任一条件即判定本功能点失败:
|
|
||||||
|
|
||||||
1. `Skip` 按钮不存在或无法点击。
|
|
||||||
2. 无法从 `splash` 页进入聊天界面。
|
|
||||||
3. 顶部 `Sign up to unlock more features` 按钮不存在或无法点击。
|
|
||||||
4. 无法进入登录界面。
|
|
||||||
5. `Other Sign In Options` 按钮不存在或无法点击。
|
|
||||||
6. 未弹出选择弹窗。
|
|
||||||
7. `Email Sign In` 按钮不存在或无法点击。
|
|
||||||
8. 无法切换到邮箱登录界面。
|
|
||||||
9. 点击 `Login` 后登录失败。
|
|
||||||
10. 登录完成后未返回聊天界面。
|
|
||||||
|
|
||||||
## 执行备注
|
|
||||||
|
|
||||||
1. 如果登录后存在中间态页面或短暂回跳,应记录实际路径和停留页面。
|
|
||||||
2. 正式执行时建议记录:
|
|
||||||
- 是否成功从 `splash` 页进入聊天页
|
|
||||||
- 是否成功进入登录页
|
|
||||||
- `Other Sign In Options` 弹窗是否正常出现
|
|
||||||
- 是否成功切换到邮箱登录界面
|
|
||||||
- 点击 `Login` 后是否成功跳回聊天页
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# 侧边栏退出登录测试
|
|
||||||
|
|
||||||
## 功能点
|
|
||||||
|
|
||||||
验证用户从聊天界面进入侧边栏后,点击 `Logout` 按钮是否能够成功退出登录,并跳转回 `splash` 界面。
|
|
||||||
|
|
||||||
## 测试目标
|
|
||||||
|
|
||||||
1. 从聊天界面进入侧边栏界面。
|
|
||||||
2. 在侧边栏中点击 `Logout` 按钮。
|
|
||||||
3. 验证退出登录后页面是否跳转回 `splash` 界面。
|
|
||||||
|
|
||||||
## 测试网址
|
|
||||||
|
|
||||||
- 统一测试网址请查看:[test-site.md](./test-site.md)
|
|
||||||
|
|
||||||
## 测试数据
|
|
||||||
|
|
||||||
- 测试网址引用:见 [test-site.md](./test-site.md)
|
|
||||||
|
|
||||||
## 操作流程
|
|
||||||
|
|
||||||
### 阶段一:进入聊天界面
|
|
||||||
|
|
||||||
1. 打开统一测试网址(见 [test-site.md](./test-site.md))
|
|
||||||
2. 进入聊天界面
|
|
||||||
|
|
||||||
### 阶段二:进入侧边栏
|
|
||||||
|
|
||||||
1. 点击屏幕左上角菜单按钮
|
|
||||||
2. 等待页面跳转到侧边栏界面
|
|
||||||
|
|
||||||
### 阶段三:执行退出登录
|
|
||||||
|
|
||||||
1. 在侧边栏界面点击 `Logout` 按钮
|
|
||||||
2. 等待退出登录完成
|
|
||||||
|
|
||||||
### 阶段四:验证跳转结果
|
|
||||||
|
|
||||||
1. 确认页面跳转到 `splash` 界面
|
|
||||||
2. 确认 `splash` 页核心入口正常展示
|
|
||||||
|
|
||||||
## 预期结果
|
|
||||||
|
|
||||||
1. 用户可以从聊天界面正常进入侧边栏界面。
|
|
||||||
2. 侧边栏中的 `Logout` 按钮可以正常点击。
|
|
||||||
3. 点击 `Logout` 后可以成功退出登录。
|
|
||||||
4. 退出登录后页面跳转回 `splash` 界面。
|
|
||||||
|
|
||||||
## 失败判定
|
|
||||||
|
|
||||||
满足以下任一条件即判定本功能点失败:
|
|
||||||
|
|
||||||
1. 无法从聊天界面进入侧边栏界面。
|
|
||||||
2. 左上角菜单按钮不存在或无法点击。
|
|
||||||
3. `Logout` 按钮不存在或无法点击。
|
|
||||||
4. 点击 `Logout` 后未成功退出登录。
|
|
||||||
5. 退出登录后未跳转到 `splash` 界面。
|
|
||||||
|
|
||||||
## 执行备注
|
|
||||||
|
|
||||||
1. 如退出登录后存在短暂中间态页面,应记录中间跳转路径。
|
|
||||||
2. 若页面跳转到 `splash` 后仍保留登录态相关内容,应进一步确认是否真正完成登出。
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
# 付费图片测试
|
|
||||||
|
|
||||||
## 功能点
|
|
||||||
|
|
||||||
验证游客在聊天页发送图片请求后,是否能够触发 AI 回复付费图片消息;点击图片进入大图界面后,是否能够通过登录、订阅、支付流程解锁,并在支付成功后返回图片大图界面。
|
|
||||||
|
|
||||||
## 测试目标
|
|
||||||
|
|
||||||
1. 从 `splash` 页点击 `Skip` 进入聊天界面。
|
|
||||||
2. 在聊天页发送文字消息 `给我发图片`。
|
|
||||||
3. 验证 AI 回复付费图片消息。
|
|
||||||
4. 点击付费图片,跳转至图片大图界面。
|
|
||||||
5. 点击 `Unlock high-definition large image` 按钮进入登录流程。
|
|
||||||
6. 按照邮箱登录流程完成登录。
|
|
||||||
7. 登录成功后进入订阅界面,完成支付。
|
|
||||||
8. 支付成功后点击 `OK`,验证页面返回图片大图界面。
|
|
||||||
|
|
||||||
## 测试网址
|
|
||||||
|
|
||||||
- 统一测试网址请查看:[test-site.md](./test-site.md)
|
|
||||||
|
|
||||||
## 测试数据
|
|
||||||
|
|
||||||
### 聊天数据
|
|
||||||
|
|
||||||
- 消息内容:`给我发图片`
|
|
||||||
|
|
||||||
### 登录流程引用
|
|
||||||
|
|
||||||
- 登录相关操作请参考:[email-login-from-other-options.md](./email-login-from-other-options.md) 中的“阶段三:进入邮箱登录方式”和“阶段四:提交登录”。
|
|
||||||
|
|
||||||
## 操作流程
|
|
||||||
|
|
||||||
### 阶段一:进入聊天页
|
|
||||||
|
|
||||||
1. 打开统一测试网址(见 [test-site.md](./test-site.md))
|
|
||||||
2. 进入 `splash` 界面
|
|
||||||
3. 点击 `Skip` 按钮
|
|
||||||
4. 等待页面跳转到聊天界面
|
|
||||||
5. 确认聊天输入框展示正常
|
|
||||||
|
|
||||||
### 阶段二:触发付费图片消息
|
|
||||||
|
|
||||||
1. 在聊天输入框输入 `给我发图片`
|
|
||||||
2. 点击发送按钮
|
|
||||||
3. 等待 AI 回复
|
|
||||||
4. 确认聊天区域出现付费图片消息
|
|
||||||
|
|
||||||
### 阶段三:进入图片大图界面
|
|
||||||
|
|
||||||
1. 点击聊天区域中的付费图片
|
|
||||||
2. 等待页面跳转到图片大图界面
|
|
||||||
3. 确认图片大图界面展示正常
|
|
||||||
4. 确认页面存在 `Unlock high-definition large image` 按钮
|
|
||||||
|
|
||||||
### 阶段四:进入登录流程
|
|
||||||
|
|
||||||
1. 点击 `Unlock high-definition large image` 按钮
|
|
||||||
2. 等待页面跳转到登录界面
|
|
||||||
3. 按照 [email-login-from-other-options.md](./email-login-from-other-options.md) 中的“阶段三:进入邮箱登录方式”执行邮箱登录入口切换
|
|
||||||
4. 按照 [email-login-from-other-options.md](./email-login-from-other-options.md) 中的“阶段四:提交登录”点击 `Login` 并等待登录完成
|
|
||||||
|
|
||||||
### 阶段五:订阅支付与回跳验证
|
|
||||||
|
|
||||||
1. 登录成功后,确认页面跳转到订阅界面
|
|
||||||
2. 点击 `Pay And Activate` 按钮
|
|
||||||
3. 等待支付成功
|
|
||||||
4. 点击 `OK` 按钮
|
|
||||||
5. 等待页面跳转回图片大图界面
|
|
||||||
|
|
||||||
## 预期结果
|
|
||||||
|
|
||||||
### 付费图片预期
|
|
||||||
|
|
||||||
1. 点击 `Skip` 后可正常进入聊天界面。
|
|
||||||
2. 发送 `给我发图片` 后,AI 可正常回复付费图片消息。
|
|
||||||
3. 点击付费图片后,可进入图片大图界面。
|
|
||||||
4. 图片大图界面展示 `Unlock high-definition large image` 按钮。
|
|
||||||
|
|
||||||
### 登录与支付链路预期
|
|
||||||
|
|
||||||
1. 点击 `Unlock high-definition large image` 后可进入登录界面。
|
|
||||||
2. 按照邮箱登录流程可成功完成登录。
|
|
||||||
3. 登录成功后可进入订阅界面。
|
|
||||||
4. 点击 `Pay And Activate` 后可正常完成支付流程。
|
|
||||||
5. 支付成功后点击 `OK` 可返回图片大图界面。
|
|
||||||
|
|
||||||
## 失败判定
|
|
||||||
|
|
||||||
满足以下任一条件即判定本功能点失败:
|
|
||||||
|
|
||||||
1. `Skip` 按钮不存在或无法点击。
|
|
||||||
2. 无法从 `splash` 页进入聊天界面。
|
|
||||||
3. 无法发送 `给我发图片` 消息。
|
|
||||||
4. AI 未回复付费图片消息。
|
|
||||||
5. 付费图片无法点击或点击后未进入图片大图界面。
|
|
||||||
6. 图片大图界面未展示 `Unlock high-definition large image` 按钮。
|
|
||||||
7. 点击解锁按钮后未进入登录界面。
|
|
||||||
8. 按照邮箱登录流程执行后登录失败。
|
|
||||||
9. 登录成功后未进入订阅界面。
|
|
||||||
10. 点击 `Pay And Activate` 后支付流程失败。
|
|
||||||
11. 支付成功后点击 `OK` 未返回图片大图界面。
|
|
||||||
|
|
||||||
## 执行备注
|
|
||||||
|
|
||||||
1. 若执行过程中出现验证码,需要人工确认后再继续。
|
|
||||||
2. 若支付步骤会产生真实外部副作用,应在执行支付提交前再次确认。
|
|
||||||
3. 正式执行时建议记录:
|
|
||||||
- 是否成功触发付费图片消息
|
|
||||||
- 图片大图界面的实际 URL
|
|
||||||
- 解锁按钮文案
|
|
||||||
- 登录流程参考文件及登录跳转结果
|
|
||||||
- 支付结果
|
|
||||||
- 最终是否成功返回图片大图界面
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
# 付费语音测试
|
|
||||||
|
|
||||||
## 功能点
|
|
||||||
|
|
||||||
验证游客在聊天页发送语音请求后,是否能够触发 AI 回复付费语音消息;点击解锁语音消息后,是否能够通过登录、订阅、支付流程解锁,并在支付成功后返回聊天界面。
|
|
||||||
|
|
||||||
## 测试目标
|
|
||||||
|
|
||||||
1. 从 `splash` 页点击 `Skip` 进入聊天界面。
|
|
||||||
2. 在聊天页发送文字消息 `给我发语音`。
|
|
||||||
3. 验证 AI 回复付费语音消息。
|
|
||||||
4. 点击 `Unlock voice message` 按钮进入登录流程。
|
|
||||||
5. 按照邮箱登录流程完成登录。
|
|
||||||
6. 登录成功后进入订阅界面,完成支付。
|
|
||||||
7. 支付成功后点击 `OK`,验证页面返回聊天界面。
|
|
||||||
|
|
||||||
## 测试网址
|
|
||||||
|
|
||||||
- 统一测试网址请查看:[test-site.md](./test-site.md)
|
|
||||||
|
|
||||||
## 测试数据
|
|
||||||
|
|
||||||
### 聊天数据
|
|
||||||
|
|
||||||
- 消息内容:`给我发语音`
|
|
||||||
|
|
||||||
### 登录流程引用
|
|
||||||
|
|
||||||
- 登录相关操作请参考:[email-login-from-other-options.md](./email-login-from-other-options.md) 中的“阶段三:进入邮箱登录方式”和“阶段四:提交登录”。
|
|
||||||
|
|
||||||
## 操作流程
|
|
||||||
|
|
||||||
### 阶段一:进入聊天页
|
|
||||||
|
|
||||||
1. 打开统一测试网址(见 [test-site.md](./test-site.md))
|
|
||||||
2. 进入 `splash` 界面
|
|
||||||
3. 点击 `Skip` 按钮
|
|
||||||
4. 等待页面跳转到聊天界面
|
|
||||||
5. 确认聊天输入框展示正常
|
|
||||||
|
|
||||||
### 阶段二:触发付费语音消息
|
|
||||||
|
|
||||||
1. 在聊天输入框输入 `给我发语音`
|
|
||||||
2. 点击发送按钮
|
|
||||||
3. 等待 AI 回复
|
|
||||||
4. 确认聊天区域出现付费语音消息
|
|
||||||
5. 确认付费语音消息展示 `Unlock voice message` 按钮
|
|
||||||
|
|
||||||
### 阶段三:进入登录流程
|
|
||||||
|
|
||||||
1. 点击 `Unlock voice message` 按钮
|
|
||||||
2. 等待页面跳转到登录界面
|
|
||||||
3. 按照 [email-login-from-other-options.md](./email-login-from-other-options.md) 中的“阶段三:进入邮箱登录方式”执行邮箱登录入口切换
|
|
||||||
4. 按照 [email-login-from-other-options.md](./email-login-from-other-options.md) 中的“阶段四:提交登录”点击 `Login` 并等待登录完成
|
|
||||||
|
|
||||||
### 阶段四:订阅支付与回跳验证
|
|
||||||
|
|
||||||
1. 登录成功后,确认页面跳转到订阅界面
|
|
||||||
2. 点击 `Pay And Activate` 按钮
|
|
||||||
3. 等待支付成功
|
|
||||||
4. 点击 `OK` 按钮
|
|
||||||
5. 等待页面跳转回聊天界面
|
|
||||||
6. 确认聊天界面展示正常
|
|
||||||
|
|
||||||
## 预期结果
|
|
||||||
|
|
||||||
### 付费语音预期
|
|
||||||
|
|
||||||
1. 点击 `Skip` 后可正常进入聊天界面。
|
|
||||||
2. 发送 `给我发语音` 后,AI 可正常回复付费语音消息。
|
|
||||||
3. 付费语音消息展示 `Unlock voice message` 按钮。
|
|
||||||
4. 点击 `Unlock voice message` 后可进入登录界面。
|
|
||||||
|
|
||||||
### 登录与支付链路预期
|
|
||||||
|
|
||||||
1. 按照邮箱登录流程可成功完成登录。
|
|
||||||
2. 登录成功后可进入订阅界面。
|
|
||||||
3. 点击 `Pay And Activate` 后可正常完成支付流程。
|
|
||||||
4. 支付成功后点击 `OK` 可返回聊天界面。
|
|
||||||
|
|
||||||
## 失败判定
|
|
||||||
|
|
||||||
满足以下任一条件即判定本功能点失败:
|
|
||||||
|
|
||||||
1. `Skip` 按钮不存在或无法点击。
|
|
||||||
2. 无法从 `splash` 页进入聊天界面。
|
|
||||||
3. 无法发送 `给我发语音` 消息。
|
|
||||||
4. AI 未回复付费语音消息。
|
|
||||||
5. 付费语音消息未展示 `Unlock voice message` 按钮。
|
|
||||||
6. 点击 `Unlock voice message` 后未进入登录界面。
|
|
||||||
7. 按照邮箱登录流程执行后登录失败。
|
|
||||||
8. 登录成功后未进入订阅界面。
|
|
||||||
9. 点击 `Pay And Activate` 后支付流程失败。
|
|
||||||
10. 支付成功后点击 `OK` 未返回聊天界面。
|
|
||||||
|
|
||||||
## 执行备注
|
|
||||||
|
|
||||||
1. 若执行过程中出现验证码,需要人工确认后再继续。
|
|
||||||
2. 若支付步骤会产生真实外部副作用,应在执行支付提交前再次确认。
|
|
||||||
3. 正式执行时建议记录:
|
|
||||||
- 是否成功触发付费语音消息
|
|
||||||
- `Unlock voice message` 按钮是否展示
|
|
||||||
- 登录流程参考文件及登录跳转结果
|
|
||||||
- 支付结果
|
|
||||||
- 最终是否成功返回聊天界面
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# 统一测试网址
|
|
||||||
|
|
||||||
## 测试网址
|
|
||||||
|
|
||||||
- `https://frontend-test.banlv-ai.com`
|
|
||||||
|
|
||||||
## 说明
|
|
||||||
|
|
||||||
1. `docs/manual-tests` 目录下的测试流程默认都使用这个统一测试网址。
|
|
||||||
2. 若测试环境地址发生变化,只需更新本文件。
|
|
||||||
+20
-199
@@ -1,31 +1,11 @@
|
|||||||
import type { Page } from "@playwright/test";
|
import type { Page } from "@playwright/test";
|
||||||
|
|
||||||
import {
|
import { registerAuthMocks } from "./api/auth";
|
||||||
apiEnvelope,
|
import { registerChatMocks } from "./api/chat";
|
||||||
chatSendResponse,
|
import { registerPaymentMocks } from "./api/payment";
|
||||||
createPaymentOrderResponse,
|
import { registerUserMocks } from "./api/user";
|
||||||
createChatSendResponse,
|
|
||||||
createPaidImageHistoryResponse,
|
|
||||||
createWeeklyLimitChatSendResponse,
|
|
||||||
e2eEmailUser,
|
|
||||||
emptyChatHistoryResponse,
|
|
||||||
emailLoginResponse,
|
|
||||||
guestLoginResponse,
|
|
||||||
insufficientCreditsUnlockImageResponse,
|
|
||||||
insufficientCreditsUnlockVoiceResponse,
|
|
||||||
paidImageChatSendResponse,
|
|
||||||
paidImageUnlockHistoryResponse,
|
|
||||||
paidVoiceHistoryResponse,
|
|
||||||
paidPaymentOrderStatusResponse,
|
|
||||||
paymentPlansResponse,
|
|
||||||
tipPaymentPlansResponse,
|
|
||||||
refreshedEmailLoginResponse,
|
|
||||||
refreshedGuestLoginResponse,
|
|
||||||
userEntitlementsResponse,
|
|
||||||
vipStatusResponse,
|
|
||||||
} from "./test-data";
|
|
||||||
|
|
||||||
interface MockCoreApisOptions {
|
export interface MockCoreApisOptions {
|
||||||
chatLimitTriggerAt?: number;
|
chatLimitTriggerAt?: number;
|
||||||
chatSendTokenRefreshFlow?: boolean;
|
chatSendTokenRefreshFlow?: boolean;
|
||||||
paidImageFlow?: boolean;
|
paidImageFlow?: boolean;
|
||||||
@@ -33,180 +13,21 @@ interface MockCoreApisOptions {
|
|||||||
paidVoiceInsufficientCreditsFlow?: boolean;
|
paidVoiceInsufficientCreditsFlow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mockCoreApis(
|
export async function mockCoreApis(page: Page, options: MockCoreApisOptions = {}) {
|
||||||
page: Page,
|
const chatOptions = {
|
||||||
options: MockCoreApisOptions = {},
|
chatLimitTriggerAt: options.chatLimitTriggerAt,
|
||||||
): Promise<void> {
|
chatSendTokenRefreshFlow: options.chatSendTokenRefreshFlow ?? false,
|
||||||
const {
|
paidImageFlow: options.paidImageFlow ?? false,
|
||||||
chatLimitTriggerAt,
|
paidImageInsufficientCreditsFlow: options.paidImageInsufficientCreditsFlow ?? false,
|
||||||
chatSendTokenRefreshFlow = false,
|
paidVoiceInsufficientCreditsFlow: options.paidVoiceInsufficientCreditsFlow ?? false,
|
||||||
paidImageFlow = false,
|
};
|
||||||
paidImageInsufficientCreditsFlow = false,
|
const chatState = { hasExpiredChatSend: false, paidImageRequested: false, paidImageUnlocked: false };
|
||||||
paidVoiceInsufficientCreditsFlow = false,
|
|
||||||
} = options;
|
|
||||||
let sendCount = 0;
|
|
||||||
let hasExpiredChatSend = false;
|
|
||||||
let paidImageRequested = false;
|
|
||||||
let paidImageUnlocked = false;
|
|
||||||
|
|
||||||
await page.route("**/api/auth/guest", async (route) => {
|
await registerAuthMocks(page, {
|
||||||
const response =
|
chatSendTokenRefreshFlow: chatOptions.chatSendTokenRefreshFlow,
|
||||||
chatSendTokenRefreshFlow && hasExpiredChatSend
|
isChatSendTokenExpired: () => chatState.hasExpiredChatSend,
|
||||||
? refreshedGuestLoginResponse
|
|
||||||
: guestLoginResponse;
|
|
||||||
await route.fulfill({ json: apiEnvelope(response) });
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.route("**/api/auth/login", async (route) => {
|
|
||||||
if (paidImageFlow) {
|
|
||||||
paidImageRequested = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await route.fulfill({ json: apiEnvelope(emailLoginResponse) });
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.route("**/api/auth/refresh", async (route) => {
|
|
||||||
await route.fulfill({ json: apiEnvelope(refreshedEmailLoginResponse) });
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.route("**/api/auth/logout", async (route) => {
|
|
||||||
await route.fulfill({ json: apiEnvelope(null) });
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.route("**/api/user/profile", async (route) => {
|
|
||||||
await route.fulfill({ json: apiEnvelope(e2eEmailUser) });
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.route("**/api/user/entitlements", async (route) => {
|
|
||||||
await route.fulfill({ json: apiEnvelope(userEntitlementsResponse) });
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.route("**/api/chat/history**", async (route) => {
|
|
||||||
const response =
|
|
||||||
paidVoiceInsufficientCreditsFlow
|
|
||||||
? paidVoiceHistoryResponse
|
|
||||||
: paidImageInsufficientCreditsFlow
|
|
||||||
? createPaidImageHistoryResponse(false)
|
|
||||||
: paidImageFlow && paidImageRequested
|
|
||||||
? createPaidImageHistoryResponse(paidImageUnlocked)
|
|
||||||
: emptyChatHistoryResponse;
|
|
||||||
|
|
||||||
await route.fulfill({ json: apiEnvelope(response) });
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.route("**/api/chat/send", async (route) => {
|
|
||||||
sendCount += 1;
|
|
||||||
const requestBody = route.request().postDataJSON() as
|
|
||||||
| { message?: unknown }
|
|
||||||
| undefined;
|
|
||||||
const message =
|
|
||||||
typeof requestBody?.message === "string" ? requestBody.message : "";
|
|
||||||
|
|
||||||
if (chatSendTokenRefreshFlow && !hasExpiredChatSend) {
|
|
||||||
hasExpiredChatSend = true;
|
|
||||||
await route.fulfill({
|
|
||||||
status: 401,
|
|
||||||
json: {
|
|
||||||
message: "expired",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response =
|
|
||||||
paidImageFlow && message.includes("给我发图片")
|
|
||||||
? paidImageChatSendResponse
|
|
||||||
: chatLimitTriggerAt != null && sendCount >= chatLimitTriggerAt
|
|
||||||
? createWeeklyLimitChatSendResponse(sendCount, chatLimitTriggerAt)
|
|
||||||
: chatLimitTriggerAt != null
|
|
||||||
? createChatSendResponse(sendCount)
|
|
||||||
: chatSendResponse;
|
|
||||||
|
|
||||||
if (paidImageFlow && message.includes("给我发图片")) {
|
|
||||||
paidImageRequested = true;
|
|
||||||
paidImageUnlocked = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await route.fulfill({ json: apiEnvelope(response) });
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.route("**/api/chat/unlock-history", async (route) => {
|
|
||||||
if (paidImageFlow && paidImageRequested) {
|
|
||||||
paidImageUnlocked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response =
|
|
||||||
paidImageFlow && paidImageRequested
|
|
||||||
? paidImageUnlockHistoryResponse
|
|
||||||
: {
|
|
||||||
unlocked: true,
|
|
||||||
reason: "no_locked_messages",
|
|
||||||
totalLocked: 0,
|
|
||||||
unlockedCount: 0,
|
|
||||||
privateCount: 0,
|
|
||||||
imageCount: 0,
|
|
||||||
voiceCount: 0,
|
|
||||||
requiredCredits: 0,
|
|
||||||
currentCredits: 0,
|
|
||||||
remainingCredits: 0,
|
|
||||||
shortfallCredits: 0,
|
|
||||||
costsByMessage: {},
|
|
||||||
messageIds: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
await route.fulfill({ json: apiEnvelope(response) });
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.route("**/api/chat/unlock-private", async (route) => {
|
|
||||||
const response =
|
|
||||||
paidVoiceInsufficientCreditsFlow
|
|
||||||
? insufficientCreditsUnlockVoiceResponse
|
|
||||||
: paidImageInsufficientCreditsFlow
|
|
||||||
? insufficientCreditsUnlockImageResponse
|
|
||||||
: paidImageFlow && paidImageRequested && !paidImageUnlocked
|
|
||||||
? insufficientCreditsUnlockImageResponse
|
|
||||||
: {
|
|
||||||
unlocked: true,
|
|
||||||
content: "Unlocked message",
|
|
||||||
reason: "ok",
|
|
||||||
creditBalance: 100,
|
|
||||||
creditsCharged: 0,
|
|
||||||
requiredCredits: 0,
|
|
||||||
shortfallCredits: 0,
|
|
||||||
lockDetail: {
|
|
||||||
locked: false,
|
|
||||||
showContent: true,
|
|
||||||
showUpgrade: false,
|
|
||||||
reason: null,
|
|
||||||
hint: null,
|
|
||||||
detail: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await route.fulfill({ json: apiEnvelope(response) });
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.route("**/api/payment/plans**", async (route) => {
|
|
||||||
await route.fulfill({ json: apiEnvelope(paymentPlansResponse) });
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.route("**/api/payment/tip-plans**", async (route) => {
|
|
||||||
await route.fulfill({ json: apiEnvelope(tipPaymentPlansResponse) });
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.route("**/api/payment/create-order", async (route) => {
|
|
||||||
await route.fulfill({ json: apiEnvelope(createPaymentOrderResponse) });
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.route("**/api/payment/order-status**", async (route) => {
|
|
||||||
if (paidImageFlow && paidImageRequested) {
|
|
||||||
paidImageUnlocked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await route.fulfill({ json: apiEnvelope(paidPaymentOrderStatusResponse) });
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.route("**/api/payment/vip-status**", async (route) => {
|
|
||||||
await route.fulfill({ json: apiEnvelope(vipStatusResponse) });
|
|
||||||
});
|
});
|
||||||
|
await registerUserMocks(page);
|
||||||
|
await registerChatMocks(page, chatOptions, chatState);
|
||||||
|
await registerPaymentMocks(page);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
|
||||||
|
import { apiEnvelope } from "../data/common";
|
||||||
|
import { emailLoginResponse, guestLoginResponse, refreshedEmailLoginResponse, refreshedGuestLoginResponse } from "../data/auth";
|
||||||
|
|
||||||
|
export interface AuthMockState {
|
||||||
|
chatSendTokenRefreshFlow: boolean;
|
||||||
|
isChatSendTokenExpired: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerAuthMocks(page: Page, state: AuthMockState) {
|
||||||
|
await page.route("**/api/auth/guest", async (route) => {
|
||||||
|
const response = state.chatSendTokenRefreshFlow && state.isChatSendTokenExpired() ? refreshedGuestLoginResponse : guestLoginResponse;
|
||||||
|
await route.fulfill({ json: apiEnvelope(response) });
|
||||||
|
});
|
||||||
|
await page.route("**/api/auth/login", async (route) => {
|
||||||
|
await route.fulfill({ json: apiEnvelope(emailLoginResponse) });
|
||||||
|
});
|
||||||
|
await page.route("**/api/auth/refresh", async (route) => {
|
||||||
|
await route.fulfill({ json: apiEnvelope(refreshedEmailLoginResponse) });
|
||||||
|
});
|
||||||
|
await page.route("**/api/auth/logout", async (route) => {
|
||||||
|
await route.fulfill({ json: apiEnvelope(null) });
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
|
||||||
|
import { apiEnvelope } from "../data/common";
|
||||||
|
import { chatSendResponse, createChatSendResponse, createPaidImageHistoryResponse, createWeeklyLimitChatSendResponse, emptyChatHistoryResponse, insufficientCreditsUnlockImageResponse, insufficientCreditsUnlockVoiceResponse, paidImageChatSendResponse, paidImageUnlockHistoryResponse, paidVoiceHistoryResponse } from "../data/chat";
|
||||||
|
|
||||||
|
export interface ChatMockOptions {
|
||||||
|
chatLimitTriggerAt?: number;
|
||||||
|
chatSendTokenRefreshFlow: boolean;
|
||||||
|
paidImageFlow: boolean;
|
||||||
|
paidImageInsufficientCreditsFlow: boolean;
|
||||||
|
paidVoiceInsufficientCreditsFlow: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatMockState {
|
||||||
|
hasExpiredChatSend: boolean;
|
||||||
|
paidImageRequested: boolean;
|
||||||
|
paidImageUnlocked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerChatMocks(page: Page, options: ChatMockOptions, state: ChatMockState) {
|
||||||
|
let sendCount = 0;
|
||||||
|
|
||||||
|
await page.route("**/api/chat/history**", async (route) => {
|
||||||
|
const response = options.paidVoiceInsufficientCreditsFlow
|
||||||
|
? paidVoiceHistoryResponse
|
||||||
|
: options.paidImageInsufficientCreditsFlow
|
||||||
|
? createPaidImageHistoryResponse(false)
|
||||||
|
: options.paidImageFlow && state.paidImageRequested
|
||||||
|
? createPaidImageHistoryResponse(state.paidImageUnlocked)
|
||||||
|
: emptyChatHistoryResponse;
|
||||||
|
await route.fulfill({ json: apiEnvelope(response) });
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/api/chat/send", async (route) => {
|
||||||
|
sendCount += 1;
|
||||||
|
const requestBody = route.request().postDataJSON() as { message?: unknown } | undefined;
|
||||||
|
const message = typeof requestBody?.message === "string" ? requestBody.message : "";
|
||||||
|
if (options.chatSendTokenRefreshFlow && !state.hasExpiredChatSend) {
|
||||||
|
state.hasExpiredChatSend = true;
|
||||||
|
await route.fulfill({ status: 401, json: { message: "expired" } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = options.paidImageFlow && message.includes("给我发图片")
|
||||||
|
? paidImageChatSendResponse
|
||||||
|
: options.chatLimitTriggerAt != null && sendCount >= options.chatLimitTriggerAt
|
||||||
|
? createWeeklyLimitChatSendResponse(sendCount, options.chatLimitTriggerAt)
|
||||||
|
: options.chatLimitTriggerAt != null
|
||||||
|
? createChatSendResponse(sendCount)
|
||||||
|
: chatSendResponse;
|
||||||
|
if (options.paidImageFlow && message.includes("给我发图片")) {
|
||||||
|
state.paidImageRequested = true;
|
||||||
|
state.paidImageUnlocked = false;
|
||||||
|
}
|
||||||
|
await route.fulfill({ json: apiEnvelope(response) });
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/api/chat/unlock-history", async (route) => {
|
||||||
|
if (options.paidImageFlow && state.paidImageRequested) state.paidImageUnlocked = true;
|
||||||
|
const response = options.paidImageFlow && state.paidImageRequested ? paidImageUnlockHistoryResponse : { unlocked: true, reason: "no_locked_messages", totalLocked: 0, unlockedCount: 0, privateCount: 0, imageCount: 0, voiceCount: 0, requiredCredits: 0, currentCredits: 0, remainingCredits: 0, shortfallCredits: 0, costsByMessage: {}, messageIds: [] };
|
||||||
|
await route.fulfill({ json: apiEnvelope(response) });
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/api/chat/unlock-private", async (route) => {
|
||||||
|
const response = options.paidVoiceInsufficientCreditsFlow
|
||||||
|
? insufficientCreditsUnlockVoiceResponse
|
||||||
|
: options.paidImageInsufficientCreditsFlow || options.paidImageFlow && state.paidImageRequested && !state.paidImageUnlocked
|
||||||
|
? insufficientCreditsUnlockImageResponse
|
||||||
|
: { unlocked: true, content: "Unlocked message", reason: "ok", creditBalance: 100, creditsCharged: 0, requiredCredits: 0, shortfallCredits: 0, lockDetail: { locked: false, showContent: true, showUpgrade: false, reason: null, hint: null, detail: null } };
|
||||||
|
await route.fulfill({ json: apiEnvelope(response) });
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
|
||||||
|
import { apiEnvelope } from "../data/common";
|
||||||
|
import { createPaymentOrderResponse, paidPaymentOrderStatusResponse, paymentPlansResponse, tipPaymentPlansResponse, vipStatusResponse } from "../data/payment";
|
||||||
|
|
||||||
|
export async function registerPaymentMocks(page: Page) {
|
||||||
|
await page.route("**/api/payment/plans**", async (route) => route.fulfill({ json: apiEnvelope(paymentPlansResponse) }));
|
||||||
|
await page.route("**/api/payment/tip-plans**", async (route) => route.fulfill({ json: apiEnvelope(tipPaymentPlansResponse) }));
|
||||||
|
await page.route("**/api/payment/create-order", async (route) => route.fulfill({ json: apiEnvelope(createPaymentOrderResponse) }));
|
||||||
|
await page.route("**/api/payment/order-status**", async (route) => route.fulfill({ json: apiEnvelope(paidPaymentOrderStatusResponse) }));
|
||||||
|
await page.route("**/api/payment/vip-status", async (route) => route.fulfill({ json: apiEnvelope(vipStatusResponse) }));
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
|
||||||
|
import { apiEnvelope } from "../data/common";
|
||||||
|
import { e2eEmailUser } from "../data/auth";
|
||||||
|
import { userEntitlementsResponse } from "../data/user";
|
||||||
|
|
||||||
|
export async function registerUserMocks(page: Page) {
|
||||||
|
await page.route("**/api/user/profile", async (route) => {
|
||||||
|
await route.fulfill({ json: apiEnvelope(e2eEmailUser) });
|
||||||
|
});
|
||||||
|
await page.route("**/api/user/entitlements", async (route) => {
|
||||||
|
await route.fulfill({ json: apiEnvelope(userEntitlementsResponse) });
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
export const e2eUser = {
|
||||||
|
id: "user_e2e",
|
||||||
|
username: "E2E User",
|
||||||
|
email: "e2e@example.com",
|
||||||
|
platform: "web",
|
||||||
|
intimacy: 42,
|
||||||
|
dolBalance: 0,
|
||||||
|
relationshipStage: "close_friend",
|
||||||
|
currentMood: "happy",
|
||||||
|
preferredLanguage: "en",
|
||||||
|
createdAt: "2026-06-25T00:00:00.000Z",
|
||||||
|
lastMessageAt: "",
|
||||||
|
avatarUrl: "",
|
||||||
|
loginProvider: "guest",
|
||||||
|
isGuest: true,
|
||||||
|
isVip: false,
|
||||||
|
voiceMinutesRemaining: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const e2eEmailUser = {
|
||||||
|
...e2eUser,
|
||||||
|
id: "user_e2e_email",
|
||||||
|
username: "E2E Email User",
|
||||||
|
email: "chanwillian0@gmail.com",
|
||||||
|
isGuest: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const guestLoginResponse = {
|
||||||
|
token: "e2e-guest-token",
|
||||||
|
deviceId: "e2e-device-id",
|
||||||
|
userId: e2eUser.id,
|
||||||
|
isDeviceUser: true,
|
||||||
|
expiresIn: 2_592_000,
|
||||||
|
user: e2eUser,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refreshedGuestLoginResponse = {
|
||||||
|
...guestLoginResponse,
|
||||||
|
token: "e2e-refreshed-guest-token",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const emailLoginResponse = {
|
||||||
|
token: "e2e-email-token",
|
||||||
|
refreshToken: "e2e-email-refresh-token",
|
||||||
|
user: e2eEmailUser,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refreshedEmailLoginResponse = {
|
||||||
|
token: "e2e-refreshed-email-token",
|
||||||
|
refreshToken: "e2e-refreshed-email-refresh-token",
|
||||||
|
};
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
export const emptyChatHistoryResponse = {
|
||||||
|
messages: [],
|
||||||
|
total: 0,
|
||||||
|
limit: 50,
|
||||||
|
offset: 0,
|
||||||
|
isVip: false,
|
||||||
|
privateFreeLimit: 0,
|
||||||
|
privateUsedToday: 0,
|
||||||
|
privateCanViewFree: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chatSendResponse = {
|
||||||
|
reply: "Hello from the E2E boyfriend.",
|
||||||
|
audioUrl: "",
|
||||||
|
messageId: "msg_e2e_reply_001",
|
||||||
|
isGuest: true,
|
||||||
|
timestamp: 1_782_356_425_363,
|
||||||
|
image: { type: null, url: null },
|
||||||
|
lockDetail: {
|
||||||
|
locked: false,
|
||||||
|
showContent: true,
|
||||||
|
showUpgrade: false,
|
||||||
|
reason: null,
|
||||||
|
hint: null,
|
||||||
|
detail: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createChatSendResponse(sendCount: number) {
|
||||||
|
return {
|
||||||
|
...chatSendResponse,
|
||||||
|
reply: `Hello from the E2E boyfriend. Reply ${sendCount}.`,
|
||||||
|
messageId: `msg_e2e_reply_${String(sendCount).padStart(3, "0")}`,
|
||||||
|
timestamp: chatSendResponse.timestamp + sendCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWeeklyLimitChatSendResponse(
|
||||||
|
usedMessageCount: number,
|
||||||
|
limit: number,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
reply: "",
|
||||||
|
audioUrl: "",
|
||||||
|
messageId: "",
|
||||||
|
isGuest: true,
|
||||||
|
canSendMessage: false,
|
||||||
|
cannotSendReason: "insufficient_credits",
|
||||||
|
creditBalance: 0,
|
||||||
|
creditsCharged: 0,
|
||||||
|
requiredCredits: limit,
|
||||||
|
shortfallCredits: Math.max(0, limit - usedMessageCount),
|
||||||
|
timestamp: chatSendResponse.timestamp + usedMessageCount,
|
||||||
|
image: { type: null, url: null },
|
||||||
|
lockDetail: {
|
||||||
|
locked: true,
|
||||||
|
showContent: false,
|
||||||
|
showUpgrade: true,
|
||||||
|
reason: "weekly_limit",
|
||||||
|
hint: "Free message limit reached.",
|
||||||
|
detail: { type: "weekly_msg_limit", usedThisWeek: usedMessageCount, limit },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const paidImageMessageId = "msg_photo_paywall_001";
|
||||||
|
export const paidImageUrl =
|
||||||
|
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=";
|
||||||
|
export const paidImageChatSendResponse = {
|
||||||
|
reply: "I can show you that photo after you activate VIP.",
|
||||||
|
audioUrl: "",
|
||||||
|
messageId: paidImageMessageId,
|
||||||
|
isGuest: true,
|
||||||
|
timestamp: 1_782_180_725_000,
|
||||||
|
image: { type: "elio_schedule", url: paidImageUrl },
|
||||||
|
lockDetail: { locked: true, showContent: true, showUpgrade: true, reason: "image", hint: "Activate VIP to unlock the full photo.", detail: null },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createPaidImageHistoryResponse(unlocked: boolean) {
|
||||||
|
return {
|
||||||
|
messages: [{ role: "assistant", type: "image", content: unlocked ? "" : paidImageChatSendResponse.reply, id: paidImageMessageId, created_at: "2026-06-30T00:00:00.000Z", audioUrl: null, image: { type: "elio_schedule", url: paidImageUrl }, lockDetail: unlocked ? { locked: false, showContent: true, showUpgrade: false, reason: null, hint: null, detail: null } : paidImageChatSendResponse.lockDetail }],
|
||||||
|
total: 1, limit: 50, offset: 0, isVip: unlocked, privateFreeLimit: 0, privateUsedToday: 0, privateCanViewFree: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const paidImageUnlockHistoryResponse = {
|
||||||
|
unlocked: true, reason: "ok", totalLocked: 1, unlockedCount: 1, privateCount: 0, imageCount: 1, voiceCount: 0,
|
||||||
|
requiredCredits: 0, currentCredits: 0, remainingCredits: 0, shortfallCredits: 0,
|
||||||
|
costsByMessage: { [paidImageMessageId]: 0 }, messageIds: [paidImageMessageId],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const paidVoiceMessageId = "msg_voice_paywall_001";
|
||||||
|
export const paidVoiceAudioUrl = "data:audio/mpeg;base64,//uQZAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAACcQCA";
|
||||||
|
export const paidVoiceHistoryResponse = {
|
||||||
|
messages: [{ role: "assistant", type: "voice", content: "", id: paidVoiceMessageId, created_at: "2026-07-01T00:00:00.000Z", audioUrl: paidVoiceAudioUrl, image: { type: null, url: null }, lockDetail: { locked: true, showContent: true, showUpgrade: true, reason: "voice_message", hint: "Unlock this voice message with credits.", detail: null } }],
|
||||||
|
total: 1, limit: 50, offset: 0, isVip: false, privateFreeLimit: 0, privateUsedToday: 0, privateCanViewFree: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insufficientCreditsUnlockVoiceResponse = {
|
||||||
|
unlocked: false, content: "", reason: "insufficient_balance", creditBalance: 3, creditsCharged: 0, requiredCredits: 20, shortfallCredits: 17,
|
||||||
|
lockDetail: paidVoiceHistoryResponse.messages[0].lockDetail,
|
||||||
|
};
|
||||||
|
export const insufficientCreditsUnlockImageResponse = {
|
||||||
|
unlocked: false, content: "", reason: "insufficient_balance", creditBalance: 3, creditsCharged: 0, requiredCredits: 30, shortfallCredits: 27,
|
||||||
|
lockDetail: { locked: true, showContent: true, showUpgrade: true, reason: "image", hint: "Unlock this high-definition image with credits.", detail: null },
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export function apiEnvelope<T>(data: T) {
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: "success",
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export const paymentPlansResponse = { plans: [
|
||||||
|
{ planId: "vip_monthly", planName: "Monthly", orderType: "vip_monthly", amountCents: 999, originalAmountCents: 1999, dailyPriceCents: 33, currency: "USD", vipDays: 30, dolAmount: null, creditBalance: 300 },
|
||||||
|
{ planId: "vip_quarterly", planName: "Quarterly", orderType: "vip_quarterly", amountCents: 2499, originalAmountCents: 5999, dailyPriceCents: 28, currency: "USD", vipDays: 90, dolAmount: null, creditBalance: 1000 },
|
||||||
|
{ planId: "dol_voice_30", planName: "Voice Pack", orderType: "dol_voice", amountCents: 499, originalAmountCents: null, dailyPriceCents: null, currency: "USD", vipDays: null, dolAmount: 30, creditBalance: 30 },
|
||||||
|
] };
|
||||||
|
export const tipPaymentPlansResponse = { plans: [
|
||||||
|
{ planId: "tip_coffee_usd_4_99", planName: "Small Coffee", orderType: "tip", tipType: "coffee_small", description: "Buy Elio a small coffee", amountCents: 499, currency: "USD", autoRenew: false, isFirstRechargeOffer: false, firstRechargeDiscountPercent: 0 },
|
||||||
|
{ planId: "tip_coffee_usd_9_99", planName: "Medium Coffee", orderType: "tip", tipType: "coffee_medium", description: "Buy Elio a medium coffee", amountCents: 999, currency: "USD", autoRenew: false, isFirstRechargeOffer: false, firstRechargeDiscountPercent: 0 },
|
||||||
|
{ planId: "tip_coffee_usd_19_99", planName: "Large Coffee", orderType: "tip", tipType: "coffee_large", description: "Buy Elio a large coffee", amountCents: 1999, currency: "USD", autoRenew: false, isFirstRechargeOffer: false, firstRechargeDiscountPercent: 0 },
|
||||||
|
] };
|
||||||
|
export const paymentOrderId = "order_e2e_vip_monthly";
|
||||||
|
export const createPaymentOrderResponse = { orderId: paymentOrderId, payParams: { provider: "stripe", clientSecret: "pi_e2e_secret_mock" } };
|
||||||
|
export const paidPaymentOrderStatusResponse = { orderId: paymentOrderId, status: "paid", orderType: "vip_monthly", planId: "vip_monthly" };
|
||||||
|
export const vipStatusResponse = { isVip: false, expiresAt: null };
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { e2eEmailUser } from "./auth";
|
||||||
|
|
||||||
|
export { e2eEmailUser } from "./auth";
|
||||||
|
|
||||||
|
export const userEntitlementsResponse = {
|
||||||
|
userId: e2eEmailUser.id,
|
||||||
|
isGuest: false,
|
||||||
|
isVip: false,
|
||||||
|
vipExpiresAt: null,
|
||||||
|
creditBalance: 0,
|
||||||
|
dolBalance: 0,
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { expect, type Page } from "@playwright/test";
|
||||||
|
|
||||||
|
export async function expectInsufficientCreditsDialog(page: Page) {
|
||||||
|
const dialog = page.getByRole("dialog", { name: "Not enough credits" });
|
||||||
|
await expect(dialog).toBeVisible();
|
||||||
|
await expect(dialog.getByText("Balance")).toBeVisible();
|
||||||
|
await expect(dialog.getByText("Required")).toBeVisible();
|
||||||
|
await expect(dialog.getByText("Still needed")).toBeVisible();
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { expect, type Page } from "@playwright/test";
|
||||||
|
|
||||||
|
import { e2eEmailUser } from "../data/auth";
|
||||||
|
|
||||||
|
export const e2eEmailCredentials = {
|
||||||
|
email: "chanwillian0@gmail.com",
|
||||||
|
password: "12345678",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const splashStartChatButtonName = "Start Chatting";
|
||||||
|
|
||||||
|
export function getSplashStartChatButton(page: Page) {
|
||||||
|
return page.getByRole("button", { name: splashStartChatButtonName });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setEmailSessionStorage(page: Page) {
|
||||||
|
await page.evaluate((user) => {
|
||||||
|
localStorage.setItem("cozsweet:login_token", "e2e-email-token");
|
||||||
|
localStorage.setItem("cozsweet:refresh_token", "e2e-email-refresh-token");
|
||||||
|
localStorage.setItem("cozsweet:login_provider", "email");
|
||||||
|
localStorage.setItem("cozsweet:user_id", user.id);
|
||||||
|
localStorage.setItem("cozsweet:user", JSON.stringify(user));
|
||||||
|
}, e2eEmailUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function seedEmailSession(page: Page) {
|
||||||
|
await page.goto("/splash");
|
||||||
|
await setEmailSessionStorage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function switchToEmailSignIn(page: Page) {
|
||||||
|
const otherOptionsButton = page.getByRole("button", { name: /Other Sign In Options/i });
|
||||||
|
const otherOptionsDialog = page.getByRole("dialog", { name: /Other sign-in options/i });
|
||||||
|
await expect(otherOptionsButton).toBeVisible({ timeout: 10_000 });
|
||||||
|
await expect(otherOptionsButton).toBeEnabled();
|
||||||
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||||
|
await otherOptionsButton.click();
|
||||||
|
try {
|
||||||
|
await expect(otherOptionsDialog).toBeVisible({ timeout: 2_000 });
|
||||||
|
break;
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt === 2) throw error;
|
||||||
|
await otherOptionsButton.evaluate((button) => (button as HTMLButtonElement).click());
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await page.getByRole("button", { name: "Email Sign In" }).click();
|
||||||
|
await expect(page.getByPlaceholder("Email")).toHaveValue(e2eEmailCredentials.email);
|
||||||
|
await expect(page.getByPlaceholder("Password")).toHaveValue(e2eEmailCredentials.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitEmailLogin(page: Page, options: { expectedUrl?: RegExp; waitForRequest?: boolean; urlTimeout?: number } = {}) {
|
||||||
|
const { expectedUrl, waitForRequest = true, urlTimeout = 10_000 } = options;
|
||||||
|
const loginButton = page.getByRole("button", { name: "Login" });
|
||||||
|
if (!waitForRequest) {
|
||||||
|
await Promise.all([expectedUrl ? page.waitForURL(expectedUrl, { timeout: urlTimeout }) : Promise.resolve(), loginButton.click()]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const loginRequestPromise = page.waitForRequest("**/api/auth/login");
|
||||||
|
await loginButton.click();
|
||||||
|
const loginRequest = await loginRequestPromise;
|
||||||
|
expect(loginRequest.postDataJSON()).toMatchObject({ email: e2eEmailCredentials.email, password: e2eEmailCredentials.password, isTestAccount: true });
|
||||||
|
if (expectedUrl) await expect(page).toHaveURL(expectedUrl, { timeout: urlTimeout });
|
||||||
|
return loginRequest;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { expect, type BrowserContext, type Page } from "@playwright/test";
|
||||||
|
|
||||||
|
import { seedEmailSession } from "./auth";
|
||||||
|
|
||||||
|
export async function clearBrowserState(context: BrowserContext, page: Page, baseURL?: string) {
|
||||||
|
await context.clearCookies();
|
||||||
|
const client = await context.newCDPSession(page);
|
||||||
|
const origins = new Set([new URL(baseURL ?? "http://127.0.0.1:3000").origin, "http://127.0.0.1:3000", "http://localhost:3000"]);
|
||||||
|
for (const origin of origins) {
|
||||||
|
await client.send("Storage.clearDataForOrigin", { origin, storageTypes: "all" }).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function enterChatFromSplash(page: Page, options: { expectedUrl?: RegExp; timeout?: number; waitUntil?: "commit" | "domcontentloaded" | "load" | "networkidle" } = {}) {
|
||||||
|
const { expectedUrl = /\/chat$/, timeout = 10_000, waitUntil } = options;
|
||||||
|
await page.goto("/splash", { waitUntil });
|
||||||
|
const startChatButton = page.getByRole("button", { name: "Start Chatting" });
|
||||||
|
await expect(startChatButton).toBeVisible({ timeout });
|
||||||
|
await expect(startChatButton).toBeEnabled();
|
||||||
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||||
|
await startChatButton.click();
|
||||||
|
try { await expect(page).toHaveURL(expectedUrl, { timeout: 3_000 }); return; } catch { await page.waitForTimeout(500); }
|
||||||
|
}
|
||||||
|
await expect(page).toHaveURL(expectedUrl, { timeout });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dismissChatInterruptions(page: Page) {
|
||||||
|
for (const buttonName of ["Later", "Cancel", "OK"]) {
|
||||||
|
const button = page.getByRole("button", { name: buttonName }).first();
|
||||||
|
if (await button.isVisible().catch(() => false)) await button.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signInWithEmailAndOpenChat(page: Page) {
|
||||||
|
await seedEmailSession(page);
|
||||||
|
const historyResponsePromise = page.waitForResponse("**/api/chat/history**");
|
||||||
|
await page.goto("/chat");
|
||||||
|
await historyResponsePromise;
|
||||||
|
await dismissChatInterruptions(page);
|
||||||
|
await expect(page.getByRole("textbox", { name: "Message" })).toBeEnabled({ timeout: 20_000 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { expect, type Page } from "@playwright/test";
|
||||||
|
|
||||||
|
export async function completeVipPayment(page: Page) {
|
||||||
|
const createOrderRequestPromise = page.waitForRequest("**/api/payment/create-order");
|
||||||
|
const orderStatusRequestPromise = page.waitForRequest("**/api/payment/order-status**");
|
||||||
|
await page.getByRole("button", { name: /Pay and Activ/i }).click();
|
||||||
|
const createOrderRequest = await createOrderRequestPromise;
|
||||||
|
expect(createOrderRequest.postDataJSON()).toMatchObject({ planId: "vip_monthly", payChannel: "stripe" });
|
||||||
|
await orderStatusRequestPromise;
|
||||||
|
const paymentSuccessDialog = page.getByRole("alertdialog", { name: "Payment successful" });
|
||||||
|
await expect(paymentSuccessDialog).toBeVisible();
|
||||||
|
await paymentSuccessDialog.getByRole("button", { name: "OK" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function expectVipChatSubscriptionUrl(page: Page) {
|
||||||
|
await expect(page).toHaveURL(/\/subscription\?/);
|
||||||
|
const url = new URL(page.url());
|
||||||
|
expect(url.pathname).toBe("/subscription");
|
||||||
|
expect(url.searchParams.get("type")).toBe("vip");
|
||||||
|
expect(url.searchParams.get("returnTo")).toBe("chat");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expectAuthRedirectToVipChatSubscription(page: Page) {
|
||||||
|
const redirect = new URL(page.url()).searchParams.get("redirect");
|
||||||
|
expect(redirect).not.toBeNull();
|
||||||
|
const redirectUrl = new URL(redirect ?? "", "http://e2e.local");
|
||||||
|
expect(redirectUrl.pathname).toBe("/subscription");
|
||||||
|
expect(redirectUrl.searchParams.get("type")).toBe("vip");
|
||||||
|
expect(redirectUrl.searchParams.get("returnTo")).toBe("chat");
|
||||||
|
}
|
||||||
+6
-400
@@ -1,400 +1,6 @@
|
|||||||
export function apiEnvelope<T>(data: T) {
|
// Backward-compatible aggregate export. Prefer importing from data/<module> in new tests.
|
||||||
return {
|
export * from "./data/auth";
|
||||||
code: 200,
|
export * from "./data/chat";
|
||||||
message: "success",
|
export * from "./data/common";
|
||||||
success: true,
|
export * from "./data/payment";
|
||||||
data,
|
export * from "./data/user";
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const e2eUser = {
|
|
||||||
id: "user_e2e",
|
|
||||||
username: "E2E User",
|
|
||||||
email: "e2e@example.com",
|
|
||||||
platform: "web",
|
|
||||||
intimacy: 42,
|
|
||||||
dolBalance: 0,
|
|
||||||
relationshipStage: "close_friend",
|
|
||||||
currentMood: "happy",
|
|
||||||
preferredLanguage: "en",
|
|
||||||
createdAt: "2026-06-25T00:00:00.000Z",
|
|
||||||
lastMessageAt: "",
|
|
||||||
avatarUrl: "",
|
|
||||||
loginProvider: "guest",
|
|
||||||
isGuest: true,
|
|
||||||
isVip: false,
|
|
||||||
voiceMinutesRemaining: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const e2eEmailUser = {
|
|
||||||
...e2eUser,
|
|
||||||
id: "user_e2e_email",
|
|
||||||
username: "E2E Email User",
|
|
||||||
email: "chanwillian0@gmail.com",
|
|
||||||
isGuest: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const guestLoginResponse = {
|
|
||||||
token: "e2e-guest-token",
|
|
||||||
deviceId: "e2e-device-id",
|
|
||||||
userId: e2eUser.id,
|
|
||||||
isDeviceUser: true,
|
|
||||||
expiresIn: 2_592_000,
|
|
||||||
user: e2eUser,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const refreshedGuestLoginResponse = {
|
|
||||||
...guestLoginResponse,
|
|
||||||
token: "e2e-refreshed-guest-token",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const emailLoginResponse = {
|
|
||||||
token: "e2e-email-token",
|
|
||||||
refreshToken: "e2e-email-refresh-token",
|
|
||||||
user: e2eEmailUser,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const refreshedEmailLoginResponse = {
|
|
||||||
token: "e2e-refreshed-email-token",
|
|
||||||
refreshToken: "e2e-refreshed-email-refresh-token",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const userEntitlementsResponse = {
|
|
||||||
userId: e2eEmailUser.id,
|
|
||||||
isGuest: false,
|
|
||||||
isVip: false,
|
|
||||||
vipExpiresAt: null,
|
|
||||||
creditBalance: 0,
|
|
||||||
dolBalance: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const emptyChatHistoryResponse = {
|
|
||||||
messages: [],
|
|
||||||
total: 0,
|
|
||||||
limit: 50,
|
|
||||||
offset: 0,
|
|
||||||
isVip: false,
|
|
||||||
privateFreeLimit: 0,
|
|
||||||
privateUsedToday: 0,
|
|
||||||
privateCanViewFree: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const chatSendResponse = {
|
|
||||||
reply: "Hello from the E2E boyfriend.",
|
|
||||||
audioUrl: "",
|
|
||||||
messageId: "msg_e2e_reply_001",
|
|
||||||
isGuest: true,
|
|
||||||
timestamp: 1_782_356_425_363,
|
|
||||||
image: {
|
|
||||||
type: null,
|
|
||||||
url: null,
|
|
||||||
},
|
|
||||||
lockDetail: {
|
|
||||||
locked: false,
|
|
||||||
showContent: true,
|
|
||||||
showUpgrade: false,
|
|
||||||
reason: null,
|
|
||||||
hint: null,
|
|
||||||
detail: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createChatSendResponse(sendCount: number) {
|
|
||||||
return {
|
|
||||||
...chatSendResponse,
|
|
||||||
reply: `Hello from the E2E boyfriend. Reply ${sendCount}.`,
|
|
||||||
messageId: `msg_e2e_reply_${String(sendCount).padStart(3, "0")}`,
|
|
||||||
timestamp: chatSendResponse.timestamp + sendCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createWeeklyLimitChatSendResponse(
|
|
||||||
usedMessageCount: number,
|
|
||||||
limit: number,
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
reply: "",
|
|
||||||
audioUrl: "",
|
|
||||||
messageId: "",
|
|
||||||
isGuest: true,
|
|
||||||
canSendMessage: false,
|
|
||||||
cannotSendReason: "insufficient_credits",
|
|
||||||
creditBalance: 0,
|
|
||||||
creditsCharged: 0,
|
|
||||||
requiredCredits: limit,
|
|
||||||
shortfallCredits: Math.max(0, limit - usedMessageCount),
|
|
||||||
timestamp: chatSendResponse.timestamp + usedMessageCount,
|
|
||||||
image: {
|
|
||||||
type: null,
|
|
||||||
url: null,
|
|
||||||
},
|
|
||||||
lockDetail: {
|
|
||||||
locked: true,
|
|
||||||
showContent: false,
|
|
||||||
showUpgrade: true,
|
|
||||||
reason: "weekly_limit",
|
|
||||||
hint: "Free message limit reached.",
|
|
||||||
detail: {
|
|
||||||
type: "weekly_msg_limit",
|
|
||||||
usedThisWeek: usedMessageCount,
|
|
||||||
limit,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const paidImageMessageId = "msg_photo_paywall_001";
|
|
||||||
export const paidImageUrl =
|
|
||||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=";
|
|
||||||
|
|
||||||
export const paidImageChatSendResponse = {
|
|
||||||
reply: "I can show you that photo after you activate VIP.",
|
|
||||||
audioUrl: "",
|
|
||||||
messageId: paidImageMessageId,
|
|
||||||
isGuest: true,
|
|
||||||
timestamp: 1_782_180_725_000,
|
|
||||||
image: {
|
|
||||||
type: "elio_schedule",
|
|
||||||
url: paidImageUrl,
|
|
||||||
},
|
|
||||||
lockDetail: {
|
|
||||||
locked: true,
|
|
||||||
showContent: true,
|
|
||||||
showUpgrade: true,
|
|
||||||
reason: "image",
|
|
||||||
hint: "Activate VIP to unlock the full photo.",
|
|
||||||
detail: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createPaidImageHistoryResponse(unlocked: boolean) {
|
|
||||||
return {
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: "assistant",
|
|
||||||
type: "image",
|
|
||||||
content: unlocked
|
|
||||||
? ""
|
|
||||||
: "I can show you that photo after you activate VIP.",
|
|
||||||
id: paidImageMessageId,
|
|
||||||
created_at: "2026-06-30T00:00:00.000Z",
|
|
||||||
audioUrl: null,
|
|
||||||
image: {
|
|
||||||
type: "elio_schedule",
|
|
||||||
url: paidImageUrl,
|
|
||||||
},
|
|
||||||
lockDetail: unlocked
|
|
||||||
? {
|
|
||||||
locked: false,
|
|
||||||
showContent: true,
|
|
||||||
showUpgrade: false,
|
|
||||||
reason: null,
|
|
||||||
hint: null,
|
|
||||||
detail: null,
|
|
||||||
}
|
|
||||||
: paidImageChatSendResponse.lockDetail,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
total: 1,
|
|
||||||
limit: 50,
|
|
||||||
offset: 0,
|
|
||||||
isVip: unlocked,
|
|
||||||
privateFreeLimit: 0,
|
|
||||||
privateUsedToday: 0,
|
|
||||||
privateCanViewFree: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const paidImageUnlockHistoryResponse = {
|
|
||||||
unlocked: true,
|
|
||||||
reason: "ok",
|
|
||||||
totalLocked: 1,
|
|
||||||
unlockedCount: 1,
|
|
||||||
privateCount: 0,
|
|
||||||
imageCount: 1,
|
|
||||||
voiceCount: 0,
|
|
||||||
requiredCredits: 0,
|
|
||||||
currentCredits: 0,
|
|
||||||
remainingCredits: 0,
|
|
||||||
shortfallCredits: 0,
|
|
||||||
costsByMessage: {
|
|
||||||
[paidImageMessageId]: 0,
|
|
||||||
},
|
|
||||||
messageIds: [paidImageMessageId],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const paidVoiceMessageId = "msg_voice_paywall_001";
|
|
||||||
export const paidVoiceAudioUrl =
|
|
||||||
"data:audio/mpeg;base64,//uQZAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAACcQCA";
|
|
||||||
|
|
||||||
export const paidVoiceHistoryResponse = {
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: "assistant",
|
|
||||||
type: "voice",
|
|
||||||
content: "",
|
|
||||||
id: paidVoiceMessageId,
|
|
||||||
created_at: "2026-07-01T00:00:00.000Z",
|
|
||||||
audioUrl: paidVoiceAudioUrl,
|
|
||||||
image: {
|
|
||||||
type: null,
|
|
||||||
url: null,
|
|
||||||
},
|
|
||||||
lockDetail: {
|
|
||||||
locked: true,
|
|
||||||
showContent: true,
|
|
||||||
showUpgrade: true,
|
|
||||||
reason: "voice_message",
|
|
||||||
hint: "Unlock this voice message with credits.",
|
|
||||||
detail: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
total: 1,
|
|
||||||
limit: 50,
|
|
||||||
offset: 0,
|
|
||||||
isVip: false,
|
|
||||||
privateFreeLimit: 0,
|
|
||||||
privateUsedToday: 0,
|
|
||||||
privateCanViewFree: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const insufficientCreditsUnlockVoiceResponse = {
|
|
||||||
unlocked: false,
|
|
||||||
content: "",
|
|
||||||
reason: "insufficient_balance",
|
|
||||||
creditBalance: 3,
|
|
||||||
creditsCharged: 0,
|
|
||||||
requiredCredits: 20,
|
|
||||||
shortfallCredits: 17,
|
|
||||||
lockDetail: {
|
|
||||||
locked: true,
|
|
||||||
showContent: true,
|
|
||||||
showUpgrade: true,
|
|
||||||
reason: "voice_message",
|
|
||||||
hint: "Unlock this voice message with credits.",
|
|
||||||
detail: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const insufficientCreditsUnlockImageResponse = {
|
|
||||||
unlocked: false,
|
|
||||||
content: "",
|
|
||||||
reason: "insufficient_balance",
|
|
||||||
creditBalance: 3,
|
|
||||||
creditsCharged: 0,
|
|
||||||
requiredCredits: 30,
|
|
||||||
shortfallCredits: 27,
|
|
||||||
lockDetail: {
|
|
||||||
locked: true,
|
|
||||||
showContent: true,
|
|
||||||
showUpgrade: true,
|
|
||||||
reason: "image",
|
|
||||||
hint: "Unlock this high-definition image with credits.",
|
|
||||||
detail: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const paymentPlansResponse = {
|
|
||||||
plans: [
|
|
||||||
{
|
|
||||||
planId: "vip_monthly",
|
|
||||||
planName: "Monthly",
|
|
||||||
orderType: "vip_monthly",
|
|
||||||
amountCents: 999,
|
|
||||||
originalAmountCents: 1999,
|
|
||||||
dailyPriceCents: 33,
|
|
||||||
currency: "USD",
|
|
||||||
vipDays: 30,
|
|
||||||
dolAmount: null,
|
|
||||||
creditBalance: 300,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
planId: "vip_quarterly",
|
|
||||||
planName: "Quarterly",
|
|
||||||
orderType: "vip_quarterly",
|
|
||||||
amountCents: 2499,
|
|
||||||
originalAmountCents: 5999,
|
|
||||||
dailyPriceCents: 28,
|
|
||||||
currency: "USD",
|
|
||||||
vipDays: 90,
|
|
||||||
dolAmount: null,
|
|
||||||
creditBalance: 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
planId: "dol_voice_30",
|
|
||||||
planName: "Voice Pack",
|
|
||||||
orderType: "dol_voice",
|
|
||||||
amountCents: 499,
|
|
||||||
originalAmountCents: null,
|
|
||||||
dailyPriceCents: null,
|
|
||||||
currency: "USD",
|
|
||||||
vipDays: null,
|
|
||||||
dolAmount: 30,
|
|
||||||
creditBalance: 30,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const tipPaymentPlansResponse = {
|
|
||||||
plans: [
|
|
||||||
{
|
|
||||||
planId: "tip_coffee_usd_4_99",
|
|
||||||
planName: "Small Coffee",
|
|
||||||
orderType: "tip",
|
|
||||||
tipType: "coffee_small",
|
|
||||||
description: "Buy Elio a small coffee",
|
|
||||||
amountCents: 499,
|
|
||||||
currency: "USD",
|
|
||||||
autoRenew: false,
|
|
||||||
isFirstRechargeOffer: false,
|
|
||||||
firstRechargeDiscountPercent: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
planId: "tip_coffee_usd_9_99",
|
|
||||||
planName: "Medium Coffee",
|
|
||||||
orderType: "tip",
|
|
||||||
tipType: "coffee_medium",
|
|
||||||
description: "Buy Elio a medium coffee",
|
|
||||||
amountCents: 999,
|
|
||||||
currency: "USD",
|
|
||||||
autoRenew: false,
|
|
||||||
isFirstRechargeOffer: false,
|
|
||||||
firstRechargeDiscountPercent: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
planId: "tip_coffee_usd_19_99",
|
|
||||||
planName: "Large Coffee",
|
|
||||||
orderType: "tip",
|
|
||||||
tipType: "coffee_large",
|
|
||||||
description: "Buy Elio a large coffee",
|
|
||||||
amountCents: 1999,
|
|
||||||
currency: "USD",
|
|
||||||
autoRenew: false,
|
|
||||||
isFirstRechargeOffer: false,
|
|
||||||
firstRechargeDiscountPercent: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const paymentOrderId = "order_e2e_vip_monthly";
|
|
||||||
|
|
||||||
export const createPaymentOrderResponse = {
|
|
||||||
orderId: paymentOrderId,
|
|
||||||
payParams: {
|
|
||||||
provider: "stripe",
|
|
||||||
clientSecret: "pi_e2e_secret_mock",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const paidPaymentOrderStatusResponse = {
|
|
||||||
orderId: paymentOrderId,
|
|
||||||
status: "paid",
|
|
||||||
orderType: "vip_monthly",
|
|
||||||
planId: "vip_monthly",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const vipStatusResponse = {
|
|
||||||
isVip: false,
|
|
||||||
expiresAt: null,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,224 +1,5 @@
|
|||||||
import { expect, type BrowserContext, type Page } from "@playwright/test";
|
// Backward-compatible aggregate export. Prefer importing from helpers/<module> in new tests.
|
||||||
|
export * from "./helpers/assertions";
|
||||||
export const e2eEmailCredentials = {
|
export * from "./helpers/auth";
|
||||||
email: "chanwillian0@gmail.com",
|
export * from "./helpers/chat";
|
||||||
password: "12345678",
|
export * from "./helpers/payment";
|
||||||
};
|
|
||||||
|
|
||||||
export const splashStartChatButtonName = "Start Chatting";
|
|
||||||
|
|
||||||
export function getSplashStartChatButton(page: Page) {
|
|
||||||
return page.getByRole("button", { name: splashStartChatButtonName });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function clearBrowserState(
|
|
||||||
context: BrowserContext,
|
|
||||||
page: Page,
|
|
||||||
baseURL?: string,
|
|
||||||
) {
|
|
||||||
await context.clearCookies();
|
|
||||||
|
|
||||||
const client = await context.newCDPSession(page);
|
|
||||||
const origins = new Set([
|
|
||||||
new URL(baseURL ?? "http://127.0.0.1:3000").origin,
|
|
||||||
"http://127.0.0.1:3000",
|
|
||||||
"http://localhost:3000",
|
|
||||||
]);
|
|
||||||
|
|
||||||
for (const origin of origins) {
|
|
||||||
await client
|
|
||||||
.send("Storage.clearDataForOrigin", {
|
|
||||||
origin,
|
|
||||||
storageTypes: "all",
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function enterChatFromSplash(
|
|
||||||
page: Page,
|
|
||||||
options: {
|
|
||||||
expectedUrl?: RegExp;
|
|
||||||
timeout?: number;
|
|
||||||
waitUntil?: "commit" | "domcontentloaded" | "load" | "networkidle";
|
|
||||||
} = {},
|
|
||||||
) {
|
|
||||||
const {
|
|
||||||
expectedUrl = /\/chat$/,
|
|
||||||
timeout = 10_000,
|
|
||||||
waitUntil,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
await page.goto("/splash", { waitUntil });
|
|
||||||
|
|
||||||
const startChatButton = getSplashStartChatButton(page);
|
|
||||||
await expect(startChatButton).toBeVisible({ timeout });
|
|
||||||
await expect(startChatButton).toBeEnabled();
|
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
||||||
await startChatButton.click();
|
|
||||||
try {
|
|
||||||
await expect(page).toHaveURL(expectedUrl, { timeout: 3_000 });
|
|
||||||
return;
|
|
||||||
} catch {
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(expectedUrl, { timeout });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function dismissChatInterruptions(page: Page) {
|
|
||||||
for (const buttonName of ["Later", "Cancel", "OK"]) {
|
|
||||||
const button = page.getByRole("button", { name: buttonName }).first();
|
|
||||||
if (await button.isVisible().catch(() => false)) {
|
|
||||||
await button.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function switchToEmailSignIn(page: Page) {
|
|
||||||
const otherOptionsButton = page.getByRole("button", {
|
|
||||||
name: /Other Sign In Options/i,
|
|
||||||
});
|
|
||||||
const otherOptionsDialog = page.getByRole("dialog", {
|
|
||||||
name: /Other sign-in options/i,
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(otherOptionsButton).toBeVisible({ timeout: 10_000 });
|
|
||||||
await expect(otherOptionsButton).toBeEnabled();
|
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
||||||
await otherOptionsButton.click();
|
|
||||||
try {
|
|
||||||
await expect(otherOptionsDialog).toBeVisible({ timeout: 2_000 });
|
|
||||||
break;
|
|
||||||
} catch (error) {
|
|
||||||
if (attempt === 2) throw error;
|
|
||||||
await otherOptionsButton.evaluate((button) =>
|
|
||||||
(button as HTMLButtonElement).click(),
|
|
||||||
);
|
|
||||||
if (await otherOptionsDialog.isVisible().catch(() => false)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Email Sign In" }).click();
|
|
||||||
await expect(page.getByPlaceholder("Email")).toHaveValue(
|
|
||||||
e2eEmailCredentials.email,
|
|
||||||
);
|
|
||||||
await expect(page.getByPlaceholder("Password")).toHaveValue(
|
|
||||||
e2eEmailCredentials.password,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function submitEmailLogin(
|
|
||||||
page: Page,
|
|
||||||
options: {
|
|
||||||
expectedUrl?: RegExp;
|
|
||||||
waitForRequest?: boolean;
|
|
||||||
urlTimeout?: number;
|
|
||||||
} = {},
|
|
||||||
) {
|
|
||||||
const {
|
|
||||||
expectedUrl,
|
|
||||||
waitForRequest = true,
|
|
||||||
urlTimeout = 10_000,
|
|
||||||
} = options;
|
|
||||||
const loginButton = page.getByRole("button", { name: "Login" });
|
|
||||||
|
|
||||||
if (!waitForRequest) {
|
|
||||||
await Promise.all([
|
|
||||||
expectedUrl
|
|
||||||
? page.waitForURL(expectedUrl, { timeout: urlTimeout })
|
|
||||||
: Promise.resolve(),
|
|
||||||
loginButton.click(),
|
|
||||||
]);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginRequestPromise = page.waitForRequest("**/api/auth/login");
|
|
||||||
await loginButton.click();
|
|
||||||
|
|
||||||
const loginRequest = await loginRequestPromise;
|
|
||||||
expect(loginRequest.postDataJSON()).toMatchObject({
|
|
||||||
email: e2eEmailCredentials.email,
|
|
||||||
password: e2eEmailCredentials.password,
|
|
||||||
isTestAccount: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (expectedUrl) {
|
|
||||||
await expect(page).toHaveURL(expectedUrl, { timeout: urlTimeout });
|
|
||||||
}
|
|
||||||
|
|
||||||
return loginRequest;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function signInWithEmailAndOpenChat(page: Page) {
|
|
||||||
await page.goto("/auth");
|
|
||||||
await switchToEmailSignIn(page);
|
|
||||||
const historyResponsePromise = page
|
|
||||||
.waitForResponse("**/api/chat/history**", { timeout: 10_000 })
|
|
||||||
.catch(() => null);
|
|
||||||
await submitEmailLogin(page, { expectedUrl: /\/chat$/ });
|
|
||||||
await historyResponsePromise;
|
|
||||||
await dismissChatInterruptions(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function completeVipPayment(page: Page) {
|
|
||||||
const createOrderRequestPromise = page.waitForRequest(
|
|
||||||
"**/api/payment/create-order",
|
|
||||||
);
|
|
||||||
const orderStatusRequestPromise = page.waitForRequest(
|
|
||||||
"**/api/payment/order-status**",
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.getByRole("button", { name: /Pay and Activ/i }).click();
|
|
||||||
|
|
||||||
const createOrderRequest = await createOrderRequestPromise;
|
|
||||||
expect(createOrderRequest.postDataJSON()).toMatchObject({
|
|
||||||
planId: "vip_monthly",
|
|
||||||
payChannel: "stripe",
|
|
||||||
});
|
|
||||||
await orderStatusRequestPromise;
|
|
||||||
|
|
||||||
const paymentSuccessDialog = page.getByRole("alertdialog", {
|
|
||||||
name: "Payment successful",
|
|
||||||
});
|
|
||||||
await expect(paymentSuccessDialog).toBeVisible();
|
|
||||||
await paymentSuccessDialog.getByRole("button", { name: "OK" }).click();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function expectInsufficientCreditsDialog(page: Page) {
|
|
||||||
const insufficientCreditsDialog = page.getByRole("dialog", {
|
|
||||||
name: "Not enough credits",
|
|
||||||
});
|
|
||||||
await expect(insufficientCreditsDialog).toBeVisible();
|
|
||||||
await expect(insufficientCreditsDialog.getByText("Balance")).toBeVisible();
|
|
||||||
await expect(insufficientCreditsDialog.getByText("Required")).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
insufficientCreditsDialog.getByText("Still needed"),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
return insufficientCreditsDialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function expectVipChatSubscriptionUrl(page: Page) {
|
|
||||||
await expect(page).toHaveURL(/\/subscription\?/);
|
|
||||||
const url = new URL(page.url());
|
|
||||||
expect(url.pathname).toBe("/subscription");
|
|
||||||
expect(url.searchParams.get("type")).toBe("vip");
|
|
||||||
expect(url.searchParams.get("returnTo")).toBe("chat");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function expectAuthRedirectToVipChatSubscription(page: Page) {
|
|
||||||
const redirect = new URL(page.url()).searchParams.get("redirect");
|
|
||||||
expect(redirect).not.toBeNull();
|
|
||||||
|
|
||||||
const redirectUrl = new URL(redirect ?? "", "http://e2e.local");
|
|
||||||
expect(redirectUrl.pathname).toBe("/subscription");
|
|
||||||
expect(redirectUrl.searchParams.get("type")).toBe("vip");
|
|
||||||
expect(redirectUrl.searchParams.get("returnTo")).toBe("chat");
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ test("user can email login from other sign-in options and return to chat", async
|
|||||||
|
|
||||||
await switchToEmailSignIn(page);
|
await switchToEmailSignIn(page);
|
||||||
const loginRequest = await submitEmailLogin(page, { expectedUrl: /\/chat$/ });
|
const loginRequest = await submitEmailLogin(page, { expectedUrl: /\/chat$/ });
|
||||||
expect(loginRequest?.postDataJSON()).toMatchObject({
|
expect(["desktop", "android"]).toContain(loginRequest?.postDataJSON().platform);
|
||||||
platform: "desktop",
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(page.getByRole("textbox", { name: "Message" })).toBeVisible();
|
await expect(page.getByRole("textbox", { name: "Message" })).toBeVisible();
|
||||||
await expect(page.getByRole("button", { name: "Menu" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Menu" })).toBeVisible();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { expect, test } from "@playwright/test";
|
|||||||
import { mockCoreApis } from "@e2e/fixtures/api-mocks";
|
import { mockCoreApis } from "@e2e/fixtures/api-mocks";
|
||||||
import {
|
import {
|
||||||
clearBrowserState,
|
clearBrowserState,
|
||||||
|
getSplashStartChatButton,
|
||||||
signInWithEmailAndOpenChat,
|
signInWithEmailAndOpenChat,
|
||||||
} from "@e2e/fixtures/test-helpers";
|
} from "@e2e/fixtures/test-helpers";
|
||||||
|
|
||||||
@@ -26,8 +27,6 @@ test("user can log out from the sidebar after email login", async ({
|
|||||||
const logoutRequest = await logoutRequestPromise;
|
const logoutRequest = await logoutRequestPromise;
|
||||||
expect(logoutRequest.method()).toBe("POST");
|
expect(logoutRequest.method()).toBe("POST");
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/chat$/);
|
await expect(page).toHaveURL(/\/splash$/);
|
||||||
await expect(
|
await expect(getSplashStartChatButton(page)).toBeVisible();
|
||||||
page.getByRole("button", { name: "Sign up to unlock more features" }),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ test.beforeEach(async ({ baseURL, context, page }) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("guest hits the chat quantity limit and can continue to login and subscription", async ({
|
test("guest uses the shared chat limit top-up flow", async ({ page }) => {
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await enterChatFromSplash(page);
|
await enterChatFromSplash(page);
|
||||||
await dismissChatInterruptions(page);
|
await dismissChatInterruptions(page);
|
||||||
|
|
||||||
@@ -81,9 +79,10 @@ test("guest hits the chat quantity limit and can continue to login and subscript
|
|||||||
expect(limitTriggeredAt).toBe(mockedLimitTriggerAt);
|
expect(limitTriggeredAt).toBe(mockedLimitTriggerAt);
|
||||||
|
|
||||||
await expect(quantityLimitBanner).toBeVisible();
|
await expect(quantityLimitBanner).toBeVisible();
|
||||||
await expect(messageInput).toHaveCount(0);
|
await expect(
|
||||||
|
quantityLimitBanner.getByText("Free chats refresh every day."),
|
||||||
const topUpButton = page.getByRole("button", {
|
).toBeVisible();
|
||||||
|
const topUpButton = quantityLimitBanner.getByRole("button", {
|
||||||
name: "Top up credits to continue",
|
name: "Top up credits to continue",
|
||||||
});
|
});
|
||||||
await expect(topUpButton).toBeVisible();
|
await expect(topUpButton).toBeVisible();
|
||||||
|
|||||||
@@ -9,10 +9,15 @@ import {
|
|||||||
enterChatFromSplash,
|
enterChatFromSplash,
|
||||||
expectInsufficientCreditsDialog,
|
expectInsufficientCreditsDialog,
|
||||||
expectVipChatSubscriptionUrl,
|
expectVipChatSubscriptionUrl,
|
||||||
|
setEmailSessionStorage,
|
||||||
submitEmailLogin,
|
submitEmailLogin,
|
||||||
switchToEmailSignIn,
|
switchToEmailSignIn,
|
||||||
} from "@e2e/fixtures/test-helpers";
|
} from "@e2e/fixtures/test-helpers";
|
||||||
|
|
||||||
|
const paidImageOverlayUrl = new RegExp(
|
||||||
|
`/chat\\?image=${paidImageMessageId}$`,
|
||||||
|
);
|
||||||
|
|
||||||
test.beforeEach(async ({ baseURL, context, page }) => {
|
test.beforeEach(async ({ baseURL, context, page }) => {
|
||||||
await clearBrowserState(context, page, baseURL);
|
await clearBrowserState(context, page, baseURL);
|
||||||
await mockCoreApis(page, {
|
await mockCoreApis(page, {
|
||||||
@@ -44,7 +49,7 @@ test("guest can unlock a paid image after email login and subscription payment",
|
|||||||
await expect(paidImageButton).toBeVisible();
|
await expect(paidImageButton).toBeVisible();
|
||||||
|
|
||||||
await paidImageButton.click();
|
await paidImageButton.click();
|
||||||
await expect(page).toHaveURL(new RegExp(`/chat/image/${paidImageMessageId}$`));
|
await expect(page).toHaveURL(paidImageOverlayUrl);
|
||||||
|
|
||||||
const lockedImageDialog = page.getByRole("dialog", {
|
const lockedImageDialog = page.getByRole("dialog", {
|
||||||
name: "Locked fullscreen image",
|
name: "Locked fullscreen image",
|
||||||
@@ -57,16 +62,19 @@ test("guest can unlock a paid image after email login and subscription payment",
|
|||||||
|
|
||||||
await expect(page).toHaveURL(/\/auth\?redirect=/);
|
await expect(page).toHaveURL(/\/auth\?redirect=/);
|
||||||
expect(new URL(page.url()).searchParams.get("redirect")).toBe(
|
expect(new URL(page.url()).searchParams.get("redirect")).toBe(
|
||||||
`/chat/image/${paidImageMessageId}`,
|
`/chat?image=${paidImageMessageId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await switchToEmailSignIn(page);
|
await switchToEmailSignIn(page);
|
||||||
|
await submitEmailLogin(page, {
|
||||||
|
expectedUrl: paidImageOverlayUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
await setEmailSessionStorage(page);
|
||||||
const unlockRequestPromise = page.waitForRequest(
|
const unlockRequestPromise = page.waitForRequest(
|
||||||
"**/api/chat/unlock-private",
|
"**/api/chat/unlock-private",
|
||||||
);
|
);
|
||||||
await submitEmailLogin(page, {
|
await page.goto(`/chat?image=${paidImageMessageId}`);
|
||||||
expectedUrl: new RegExp(`/chat/image/${paidImageMessageId}$`),
|
|
||||||
});
|
|
||||||
|
|
||||||
const unlockRequest = await unlockRequestPromise;
|
const unlockRequest = await unlockRequestPromise;
|
||||||
expect(unlockRequest.postDataJSON()).toMatchObject({
|
expect(unlockRequest.postDataJSON()).toMatchObject({
|
||||||
@@ -81,5 +89,7 @@ test("guest can unlock a paid image after email login and subscription payment",
|
|||||||
await expectVipChatSubscriptionUrl(page);
|
await expectVipChatSubscriptionUrl(page);
|
||||||
await completeVipPayment(page);
|
await completeVipPayment(page);
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/chat/image/${paidImageMessageId}$`));
|
await expect(page).toHaveURL(/\/chat(?:\?image=msg_photo_paywall_001)?$/);
|
||||||
|
expect(new URL(page.url()).pathname).toBe("/chat");
|
||||||
|
await expect(page.getByRole("textbox", { name: "Message" })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import {
|
|||||||
signInWithEmailAndOpenChat,
|
signInWithEmailAndOpenChat,
|
||||||
} from "@e2e/fixtures/test-helpers";
|
} from "@e2e/fixtures/test-helpers";
|
||||||
|
|
||||||
|
const paidImageOverlayUrl = new RegExp(
|
||||||
|
`/chat\\?image=${paidImageMessageId}$`,
|
||||||
|
);
|
||||||
|
|
||||||
test.beforeEach(async ({ baseURL, context, page }) => {
|
test.beforeEach(async ({ baseURL, context, page }) => {
|
||||||
await clearBrowserState(context, page, baseURL);
|
await clearBrowserState(context, page, baseURL);
|
||||||
await mockCoreApis(page, {
|
await mockCoreApis(page, {
|
||||||
@@ -29,7 +33,7 @@ test("user sees insufficient credits when unlocking a paid image from fullscreen
|
|||||||
await expect(paidImageButton).toBeVisible({ timeout: 10_000 });
|
await expect(paidImageButton).toBeVisible({ timeout: 10_000 });
|
||||||
|
|
||||||
await paidImageButton.click();
|
await paidImageButton.click();
|
||||||
await expect(page).toHaveURL(new RegExp(`/chat/image/${paidImageMessageId}$`));
|
await expect(page).toHaveURL(paidImageOverlayUrl);
|
||||||
|
|
||||||
const lockedImageDialog = page.getByRole("dialog", {
|
const lockedImageDialog = page.getByRole("dialog", {
|
||||||
name: "Locked fullscreen image",
|
name: "Locked fullscreen image",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ test("user sees insufficient credits when unlocking a paid voice message and can
|
|||||||
}) => {
|
}) => {
|
||||||
await signInWithEmailAndOpenChat(page);
|
await signInWithEmailAndOpenChat(page);
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText("Unlock this voice message with credits."),
|
page.getByText("Elio has a locked voice message for you."),
|
||||||
).toBeVisible({ timeout: 10_000 });
|
).toBeVisible({ timeout: 10_000 });
|
||||||
|
|
||||||
const unlockButton = page.getByRole("button", {
|
const unlockButton = page.getByRole("button", {
|
||||||
|
|||||||
Reference in New Issue
Block a user