userdetail.spec.ts 8.15 KB
// REQ-USR-001 / REQ-USR-002: 用户单据 E2E 关键旅程(Playwright,headless,page.route 桩后端)
// 注:均经 SPA 内导航(登录 → 用户列表 → 新增/双击行)进入单据,避免 page.goto 全量刷新丢失 Redux 登录态。
import { test, expect, type Page } from '@playwright/test';

function ok(data: unknown) {
  return JSON.stringify({ code: 0, message: 'success', data });
}

function err(code: number, message = '业务错误') {
  return JSON.stringify({ code, message, data: null });
}

const EMPLOYEES = [{ iIncrement: 3, sEmployeeName: '张三', sEmployeeNo: 'zs' }];
const PERMISSIONS = [
  { iIncrement: 1, sPermissionName: '默认显示', sPermissionCategory: '基础' },
  { iIncrement: 2, sPermissionName: '高级查看', sPermissionCategory: '基础' },
];

function makeUser(id: number, name: string) {
  return {
    id,
    sUserName: name,
    员工名: '张三',
    sUserNo: 'zs',
    部门: null,
    sUserType: '普通用户',
    sLanguage: '中文',
    iIsVoid: 0,
    tLastLoginDate: null,
    sCreator: 'admin',
    tCreateDate: '2026-01-01T00:00:00',
  };
}

async function stubAuth(page: Page) {
  await page.route('**/api/usr/companies', async (route) => {
    await route.fulfill({ status: 200, contentType: 'application/json', body: ok([{ id: 1, sCompanyName: '甲公司', sVersion: '标准版' }]) });
  });
  await page.route('**/api/usr/login', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: ok({ token: 'tk-e2e', user: { id: 1, sUserName: '朱子纯', sUserType: '超级管理员', sLanguage: '中文' } }),
    });
  });
}

async function stubLookups(page: Page) {
  await page.route('**/api/usr/employees**', async (route) => {
    await route.fulfill({ status: 200, contentType: 'application/json', body: ok(EMPLOYEES) });
  });
  await page.route('**/api/usr/permissions**', async (route) => {
    await route.fulfill({ status: 200, contentType: 'application/json', body: ok(PERMISSIONS) });
  });
}

async function loginAndGotoList(page: Page) {
  await page.goto('/login');
  await page.getByPlaceholder('请输入你的用户名').fill('admin');
  await page.getByPlaceholder('请输入你的密码').fill('secret');
  await expect(page.getByText('甲公司(标准版)')).toBeVisible();
  await page.getByRole('button', { name: /登\s*录/ }).click();
  await expect(page).toHaveURL(/\/$/);
  await page.getByRole('button', { name: '用户列表' }).click();
  await expect(page).toHaveURL(/\/usr\/users$/);
}

test.describe('用户单据关键旅程', () => {
  test('create user and return to list', async ({ page }) => {
    await stubAuth(page);
    await stubLookups(page);
    await page.route('**/api/usr/users', async (route) => {
      if (route.request().method() === 'POST') {
        await route.fulfill({ status: 200, contentType: 'application/json', body: ok({ id: 9 }) });
      } else {
        await route.fulfill({ status: 200, contentType: 'application/json', body: ok({ records: [], total: 0, pageNum: 1, pageSize: 10 }) });
      }
    });
    await loginAndGotoList(page);
    await page.getByTestId('btn-add').click();
    await expect(page).toHaveURL(/\/usr\/users\/new$/);
    await expect(page.getByTestId('userdetail-page')).toBeVisible();
    await page.getByTestId('field-username').fill('zhangsan');
    await page.getByTestId('field-userno').fill('zs');
    await page.getByTestId('select-language').locator('.ant-select-selector').click();
    await page.getByText('中文', { exact: true }).click();
    await page.getByTestId('perm-check-1').check();

    const reqPromise = page.waitForRequest((req) => req.url().includes('/api/usr/users') && req.method() === 'POST');
    await page.getByTestId('btn-save').click();
    const req = await reqPromise;
    expect(req.postData() ?? '').toContain('zhangsan');
    await expect(page.getByText('用户创建成功')).toBeVisible();
    await expect(page).toHaveURL(/\/usr\/users$/);
  });

  test('edit user prefill then save', async ({ page }) => {
    await stubAuth(page);
    await stubLookups(page);
    await page.route('**/api/usr/users/7', async (route) => {
      await route.fulfill({ status: 200, contentType: 'application/json', body: ok({ id: 7 }) }); // PUT
    });
    await page.route('**/api/usr/users**', async (route) => {
      // 列表(首屏)+ 预填等于匹配(GET)
      if (route.request().method() === 'GET') {
        await route.fulfill({ status: 200, contentType: 'application/json', body: ok({ records: [makeUser(7, 'zhangsan')], total: 1, pageNum: 1, pageSize: 10 }) });
      } else {
        await route.fallback();
      }
    });
    await loginAndGotoList(page);
    await page.getByText('zhangsan').dblclick();
    await expect(page).toHaveURL(/\/usr\/users\/7$/);
    await expect(page.getByTestId('field-username')).toHaveValue('zhangsan');
    await expect(page.getByTestId('field-username')).toBeDisabled();
    await page.getByTestId('select-language').locator('.ant-select-selector').click();
    await page.getByText('英文', { exact: true }).click();

    const reqPromise = page.waitForRequest((req) => req.url().includes('/api/usr/users/7') && req.method() === 'PUT');
    await page.getByTestId('btn-save').click();
    await reqPromise;
    await expect(page.getByText('保存成功')).toBeVisible();
    await expect(page).toHaveURL(/\/usr\/users$/);
  });

  test('username conflict shows inline error', async ({ page }) => {
    await stubAuth(page);
    await stubLookups(page);
    await page.route('**/api/usr/users', async (route) => {
      if (route.request().method() === 'POST') {
        await route.fulfill({ status: 200, contentType: 'application/json', body: err(40901, '用户名已存在') });
      } else {
        await route.fulfill({ status: 200, contentType: 'application/json', body: ok({ records: [], total: 0, pageNum: 1, pageSize: 10 }) });
      }
    });
    await loginAndGotoList(page);
    await page.getByTestId('btn-add').click();
    await page.getByTestId('field-username').fill('zhangsan');
    await page.getByTestId('field-userno').fill('zs');
    await page.getByTestId('select-language').locator('.ant-select-selector').click();
    await page.getByText('中文', { exact: true }).click();
    await page.getByTestId('btn-save').click();
    await expect(page.getByText('用户名已存在,请更换')).toBeVisible();
  });

  test('load error shows retry', async ({ page }) => {
    await stubAuth(page);
    await page.route('**/api/usr/users', async (route) => {
      await route.fulfill({ status: 200, contentType: 'application/json', body: ok({ records: [], total: 0, pageNum: 1, pageSize: 10 }) });
    });
    await page.route('**/api/usr/employees**', async (route) => {
      await route.fulfill({ status: 200, contentType: 'application/json', body: ok(EMPLOYEES) });
    });
    let permFail = true;
    await page.route('**/api/usr/permissions**', async (route) => {
      if (permFail) {
        await route.fulfill({ status: 500, contentType: 'application/json', body: '{}' });
      } else {
        await route.fulfill({ status: 200, contentType: 'application/json', body: ok(PERMISSIONS) });
      }
    });
    await loginAndGotoList(page);
    await page.getByTestId('btn-add').click();
    await expect(page.getByTestId('userdetail-loaderror')).toBeVisible();
    permFail = false;
    await page.getByTestId('userdetail-loaderror').getByRole('button', { name: '点击重试' }).click();
    await expect(page.getByTestId('userdetail-page')).toBeVisible();
  });

  test('placeholder tabs/buttons are inert', async ({ page }) => {
    await stubAuth(page);
    await stubLookups(page);
    await page.route('**/api/usr/users', async (route) => {
      await route.fulfill({ status: 200, contentType: 'application/json', body: ok({ records: [], total: 0, pageNum: 1, pageSize: 10 }) });
    });
    await loginAndGotoList(page);
    await page.getByTestId('btn-add').click();
    await expect(page.getByTestId('userdetail-page')).toBeVisible();
    await expect(page.getByRole('tab', { name: '客户查看权限' })).toHaveAttribute('aria-disabled', 'true');
    await page.getByTestId('btn-ph-删除').click();
    await expect(page.getByText('功能开发中')).toBeVisible();
  });
});