# Instructions

- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.

# Test info

- Name: cashier-full-flow.spec.ts >> 散客做单流程 >> 散客 - 单服务现金结账
- Location: tests/cashier-full-flow.spec.ts:75:7

# Error details

```
Error: expect(locator).toBeVisible() failed

Locator: getByRole('complementary').getByText('¥88.00').first()
Expected: visible
Timeout: 5000ms
Error: element(s) not found

Call log:
  - Expect "toBeVisible" with timeout 5000ms
  - waiting for getByRole('complementary').getByText('¥88.00').first()

```

```yaml
- main:
  - button "返回"
  - navigation: 1. 购物车 › 2. 小费 › 3. 付款
  - main:
    - navigation:
      - button "推荐"
      - button "服务"
      - button "产品"
      - button "储值方案"
      - button "次卡"
      - button "套餐"
    - textbox "搜索"
    - button "纯色美甲 60 min ¥88.00":
      - paragraph: 纯色美甲
      - paragraph: 60 min
      - paragraph: ¥88.00
    - button "渐变美甲 90 min ¥138.00":
      - paragraph: 渐变美甲
      - paragraph: 90 min
      - paragraph: ¥138.00
    - button "日式手绘花艺 120 min ¥268.00":
      - paragraph: 日式手绘花艺
      - paragraph: 120 min
      - paragraph: ¥268.00
    - button "日式极简线条 90 min ¥198.00":
      - paragraph: 日式极简线条
      - paragraph: 90 min
      - paragraph: ¥198.00
    - button "韩式奶油肌 90 min ¥188.00":
      - paragraph: 韩式奶油肌
      - paragraph: 90 min
      - paragraph: ¥188.00
    - button "韩式贝壳片 105 min ¥228.00":
      - paragraph: 韩式贝壳片
      - paragraph: 105 min
      - paragraph: ¥228.00
    - button "经典法式美甲 75 min ¥128.00":
      - paragraph: 经典法式美甲
      - paragraph: 75 min
      - paragraph: ¥128.00
    - button "彩色法式 90 min ¥168.00":
      - paragraph: 彩色法式
      - paragraph: 90 min
      - paragraph: ¥168.00
    - button "新娘法式甲 120 min ¥328.00":
      - paragraph: 新娘法式甲
      - paragraph: 120 min
      - paragraph: ¥328.00
    - button "自然嫁接美睫 90 min ¥198.00":
      - paragraph: 自然嫁接美睫
      - paragraph: 90 min
      - paragraph: ¥198.00
    - button "轻盈自然款 75 min ¥168.00":
      - paragraph: 轻盈自然款
      - paragraph: 75 min
      - paragraph: ¥168.00
    - button "浓密娃娃款 105 min ¥268.00":
      - paragraph: 浓密娃娃款
      - paragraph: 105 min
      - paragraph: ¥268.00
    - button "浓密芭比款 120 min ¥298.00":
      - paragraph: 浓密芭比款
      - paragraph: 120 min
      - paragraph: ¥298.00
    - button "山茶花美睫 120 min ¥328.00":
      - paragraph: 山茶花美睫
      - paragraph: 120 min
      - paragraph: ¥328.00
    - button "山茶花仙子款 135 min ¥388.00":
      - paragraph: 山茶花仙子款
      - paragraph: 135 min
      - paragraph: ¥388.00
    - button "基础手部护理 45 min ¥68.00":
      - paragraph: 基础手部护理
      - paragraph: 45 min
      - paragraph: ¥68.00
    - button "深层滋养手膜 60 min ¥128.00":
      - paragraph: 深层滋养手膜
      - paragraph: 60 min
      - paragraph: ¥128.00
    - button "手部SPA套餐 90 min ¥198.00":
      - paragraph: 手部SPA套餐
      - paragraph: 90 min
      - paragraph: ¥198.00
    - button "基础足部护理 60 min ¥88.00":
      - paragraph: 基础足部护理
      - paragraph: 60 min
      - paragraph: ¥88.00
    - button "足部SPA舒缓 90 min ¥168.00":
      - paragraph: 足部SPA舒缓
      - paragraph: 90 min
      - paragraph: ¥168.00
    - button "足部深层修复 105 min ¥228.00":
      - paragraph: 足部深层修复
      - paragraph: 105 min
      - paragraph: ¥228.00
    - button "基础修甲护理 30 min ¥38.00":
      - paragraph: 基础修甲护理
      - paragraph: 30 min
      - paragraph: ¥38.00
    - button "精修甲型设计 45 min ¥58.00":
      - paragraph: 精修甲型设计
      - paragraph: 45 min
      - paragraph: ¥58.00
    - button "甲片延长 120 min ¥268.00":
      - paragraph: 甲片延长
      - paragraph: 120 min
      - paragraph: ¥268.00
    - button "甲油胶卸除 30 min ¥38.00":
      - paragraph: 甲油胶卸除
      - paragraph: 30 min
      - paragraph: ¥38.00
    - button "光疗甲卸除 45 min ¥58.00":
      - paragraph: 光疗甲卸除
      - paragraph: 45 min
      - paragraph: ¥58.00
    - button "美甲+手部护理套餐 120 min ¥148.00":
      - paragraph: 美甲+手部护理套餐
      - paragraph: 120 min
      - paragraph: ¥148.00
    - button "日式美甲+美睫套餐 180 min ¥388.00":
      - paragraph: 日式美甲+美睫套餐
      - paragraph: 180 min
      - paragraph: ¥388.00
    - button "新娘全套美甲美睫 240 min ¥588.00":
      - paragraph: 新娘全套美甲美睫
      - paragraph: 240 min
      - paragraph: ¥588.00
    - button "足部护理+修甲套餐 105 min ¥118.00":
      - paragraph: 足部护理+修甲套餐
      - paragraph: 105 min
      - paragraph: ¥118.00
    - complementary:
      - textbox "搜索客户（名字/手机号）..."
      - paragraph: 添加项目开始结账
    - heading "纯色美甲" [level=3]
    - text: ¥88.00
    - button "close": ✕
    - text: 选择服务员工*
    - button "a alice"
    - button "e echo"
    - button "刘 刘佳"
    - button "周 周小雨"
    - button "孙 孙燕"
    - button "张 张婷婷"
    - button "李 李雪"
    - button "王 王小丽"
    - button "赵 赵敏"
    - button "陈 陈美美"
    - button "黄 黄丽丽"
    - button "取消"
    - button "请选择服务员工" [disabled]
- region "Notifications (F8)":
  - list
```

# Test source

```ts
  1   | import { test, expect, Page } from '@playwright/test';
  2   | import { setupApiMonitor, ApiError } from './helpers';
  3   | 
  4   | // echo 账号有员工身份，可以开班和收银
  5   | const EMAIL = 'echo@shboka.com';
  6   | const PASSWORD = '123456';
  7   | const STORE = 'boka-nail';
  8   | 
  9   | async function loginAndEnterStore(page: Page) {
  10  |   await page.goto('/login');
  11  |   await page.getByLabel('邮箱').fill(EMAIL);
  12  |   await page.getByLabel('密码').fill(PASSWORD);
  13  |   await page.getByRole('button', { name: '登录' }).click();
  14  |   await expect(page.getByText('选择门店')).toBeVisible({ timeout: 10000 });
  15  |   await page.getByRole('button', { name: STORE }).click();
  16  |   await page.getByRole('button', { name: '进入门店' }).click();
  17  |   await expect(page.getByRole('heading', { name: '工作台' })).toBeVisible({ timeout: 10000 });
  18  | }
  19  | 
  20  | async function enterPOS(page: Page) {
  21  |   // 导航到收银页面
  22  |   await page.goto('/pos/orders');
  23  |   await expect(page.getByRole('heading', { name: '收银' })).toBeVisible({ timeout: 10000 });
  24  | 
  25  |   // 点击收银台按钮
  26  |   await page.getByRole('button', { name: '收银台' }).click();
  27  |   await page.waitForTimeout(1000);
  28  | 
  29  |   // 如果未开班，先开班
  30  |   const openShiftBtn = page.getByRole('button', { name: '开班' });
  31  |   if (await openShiftBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
  32  |     await openShiftBtn.click();
  33  |     await page.waitForTimeout(500);
  34  |     // 输入备用金
  35  |     const amountInput = page.getByRole('spinbutton');
  36  |     if (await amountInput.isVisible({ timeout: 2000 }).catch(() => false)) {
  37  |       await amountInput.fill('500');
  38  |     }
  39  |     await page.getByRole('button', { name: '确认' }).click();
  40  |     await page.waitForTimeout(2000);
  41  |   }
  42  | }
  43  | 
  44  | async function addServiceToCart(page: Page, serviceName: string) {
  45  |   // 切换到服务分类
  46  |   await page.getByRole('button', { name: '服务' }).click();
  47  |   await page.waitForTimeout(500);
  48  |   // 点击服务
  49  |   await page.getByRole('button', { name: new RegExp(serviceName) }).click();
  50  |   await page.waitForTimeout(500);
  51  | }
  52  | 
  53  | async function goToPayment(page: Page) {
  54  |   // 购物车 → 小费
  55  |   await page.getByRole('button', { name: '继续付款 ›' }).click();
  56  |   await expect(page.getByRole('heading', { name: '小费' })).toBeVisible({ timeout: 5000 });
  57  |   // 无小费
  58  |   await page.getByRole('button', { name: '无小费' }).click();
  59  |   await page.waitForTimeout(300);
  60  |   // 小费 → 付款
  61  |   await page.getByRole('button', { name: '继续付款 ›' }).click();
  62  |   await expect(page.getByText('支付方式')).toBeVisible({ timeout: 5000 });
  63  | }
  64  | 
  65  | async function confirmPayment(page: Page) {
  66  |   await page.getByRole('button', { name: /确认收款/ }).click();
  67  |   await page.waitForTimeout(3000);
  68  | }
  69  | 
  70  | // ═══════════════════════════════════════════
  71  | // 散客做单子
  72  | // ═══════════════════════════════════════════
  73  | test.describe('散客做单流程', () => {
  74  | 
  75  |   test('散客 - 单服务现金结账', async ({ page }) => {
  76  |     const monitor = setupApiMonitor(page);
  77  |     await loginAndEnterStore(page);
  78  |     await enterPOS(page);
  79  | 
  80  |     // 不选客户 = 散客
  81  |     await addServiceToCart(page, '纯色美甲');
  82  | 
  83  |     // 验证购物车
  84  |     const cart = page.getByRole('complementary');
> 85  |     await expect(cart.getByText('¥88.00').first()).toBeVisible();
      |                                                    ^ Error: expect(locator).toBeVisible() failed
  86  | 
  87  |     // 走到付款
  88  |     await goToPayment(page);
  89  | 
  90  |     // 验证客户来源是散客
  91  |     await expect(page.getByText('散客')).toBeVisible();
  92  | 
  93  |     // 验证应收金额
  94  |     await expect(page.getByText('¥88.00').first()).toBeVisible();
  95  | 
  96  |     // 确认收款
  97  |     await confirmPayment(page);
  98  | 
  99  |     // 验证 API 无错误
  100 |     await monitor.expectNoApiErrors();
  101 |   });
  102 | 
  103 |   test('散客 - 多服务结账', async ({ page }) => {
  104 |     const monitor = setupApiMonitor(page);
  105 |     await loginAndEnterStore(page);
  106 |     await enterPOS(page);
  107 | 
  108 |     // 加两个服务
  109 |     await addServiceToCart(page, '纯色美甲');
  110 |     await addServiceToCart(page, '渐变美甲');
  111 | 
  112 |     // 验证合计 88 + 138 = 226
  113 |     const cart = page.getByRole('complementary');
  114 |     await expect(cart.getByText('¥226.00').first()).toBeVisible();
  115 | 
  116 |     // 走到付款
  117 |     await goToPayment(page);
  118 | 
  119 |     // 确认收款
  120 |     await confirmPayment(page);
  121 |     await monitor.expectNoApiErrors();
  122 |   });
  123 | 
  124 |   test('散客 - 带小费结账', async ({ page }) => {
  125 |     const monitor = setupApiMonitor(page);
  126 |     await loginAndEnterStore(page);
  127 |     await enterPOS(page);
  128 | 
  129 |     await addServiceToCart(page, '纯色美甲');
  130 | 
  131 |     // 购物车 → 小费
  132 |     await page.getByRole('button', { name: '继续付款 ›' }).click();
  133 |     await expect(page.getByRole('heading', { name: '小费' })).toBeVisible({ timeout: 5000 });
  134 | 
  135 |     // 选 10% 小费
  136 |     await page.getByRole('button', { name: '10%' }).click();
  137 |     await page.waitForTimeout(300);
  138 | 
  139 |     // 小费 → 付款
  140 |     await page.getByRole('button', { name: '继续付款 ›' }).click();
  141 |     await expect(page.getByText('支付方式')).toBeVisible({ timeout: 5000 });
  142 | 
  143 |     // 确认收款
  144 |     await confirmPayment(page);
  145 |     await monitor.expectNoApiErrors();
  146 |   });
  147 | 
  148 |   test('散客 - 购物车删除项目', async ({ page }) => {
  149 |     await loginAndEnterStore(page);
  150 |     await enterPOS(page);
  151 | 
  152 |     await addServiceToCart(page, '纯色美甲');
  153 |     await addServiceToCart(page, '渐变美甲');
  154 | 
  155 |     // 删除第一个
  156 |     const cart = page.getByRole('complementary');
  157 |     await cart.getByRole('button', { name: '删除' }).first().click();
  158 |     await page.waitForTimeout(500);
  159 | 
  160 |     // 合计应该只剩 138
  161 |     await expect(cart.getByText('¥138.00').first()).toBeVisible();
  162 |   });
  163 | 
  164 |   test('散客 - 返回不产生错误', async ({ page }) => {
  165 |     const monitor = setupApiMonitor(page);
  166 |     await loginAndEnterStore(page);
  167 |     await enterPOS(page);
  168 | 
  169 |     await addServiceToCart(page, '纯色美甲');
  170 | 
  171 |     // 返回
  172 |     await page.getByRole('button', { name: '返回' }).click();
  173 |     await expect(page.getByRole('heading', { name: '收银' })).toBeVisible({ timeout: 5000 });
  174 | 
  175 |     await monitor.expectNoApiErrors();
  176 |   });
  177 | });
  178 | 
  179 | // ═══════════════════════════════════════════
  180 | // 会员做单子
  181 | // ═══════════════════════════════════════════
  182 | test.describe('会员做单流程', () => {
  183 | 
  184 |   test('会员 - 搜索并关联会员后结账', async ({ page }) => {
  185 |     const monitor = setupApiMonitor(page);
```