region-edit.spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. import { TIMEOUTS } from '../../utils/timeouts';
  2. import { test, expect } from '../../utils/test-setup';
  3. import { readFileSync } from 'fs';
  4. import { join, dirname } from 'path';
  5. import { fileURLToPath } from 'url';
  6. const __filename = fileURLToPath(import.meta.url);
  7. const __dirname = dirname(__filename);
  8. const testUsers = JSON.parse(readFileSync(join(__dirname, '../../fixtures/test-users.json'), 'utf-8'));
  9. /**
  10. * 生成唯一区域名称
  11. * @param prefix - 名称前缀
  12. * @returns 唯一的区域名称
  13. */
  14. function generateUniqueRegionName(prefix: string = '测试区域'): string {
  15. const timestamp = Date.now();
  16. const random = Math.floor(Math.random() * 1000);
  17. return `${prefix}_${timestamp}_${random}`;
  18. }
  19. /**
  20. * 生成唯一区域代码
  21. * @param level - 区域层级
  22. * @returns 唯一的区域代码
  23. */
  24. function generateUniqueRegionCode(level: string): string {
  25. const timestamp = Date.now();
  26. return `${level.toUpperCase()}_${timestamp}`;
  27. }
  28. test.describe.serial('编辑区域测试', () => {
  29. // 用于跟踪测试创建的区域,以便清理
  30. const createdProvinces: string[] = [];
  31. test.beforeEach(async ({ adminLoginPage, regionManagementPage }) => {
  32. // 以管理员身份登录后台
  33. await adminLoginPage.goto();
  34. await adminLoginPage.login(testUsers.admin.username, testUsers.admin.password);
  35. await adminLoginPage.expectLoginSuccess();
  36. await regionManagementPage.goto();
  37. await regionManagementPage.waitForTreeLoaded();
  38. });
  39. test.afterEach(async ({ regionManagementPage }) => {
  40. // 清理测试创建的数据
  41. let cleanupSuccessCount = 0;
  42. let cleanupFailCount = 0;
  43. for (const provinceName of createdProvinces) {
  44. try {
  45. // 等待树形结构就绪后检查区域是否存在
  46. await regionManagementPage.waitForTreeLoaded();
  47. const exists = await regionManagementPage.regionExists(provinceName);
  48. if (exists) {
  49. const deleteSuccess = await regionManagementPage.deleteRegion(provinceName);
  50. if (deleteSuccess) {
  51. cleanupSuccessCount++;
  52. console.debug(`✅ 已清理测试数据: ${provinceName}`);
  53. } else {
  54. cleanupFailCount++;
  55. console.debug(`⚠️ 删除失败(无成功提示): ${provinceName}`);
  56. }
  57. } else {
  58. console.debug(`ℹ️ 区域不存在,跳过删除: ${provinceName}`);
  59. }
  60. } catch (error) {
  61. cleanupFailCount++;
  62. console.debug(`❌ 清理异常: ${provinceName}`, error);
  63. }
  64. }
  65. // 记录清理结果摘要
  66. console.debug(`🧹 测试数据清理: 成功 ${cleanupSuccessCount}, 失败 ${cleanupFailCount}`);
  67. // 如果有清理失败,记录警告但不阻塞测试
  68. if (cleanupFailCount > 0) {
  69. console.debug(`⚠️ 有 ${cleanupFailCount} 个区域清理失败,可能产生脏数据`);
  70. }
  71. // 清空跟踪列表
  72. createdProvinces.length = 0;
  73. });
  74. test.describe('编辑区域名称', () => {
  75. test('应该成功编辑区域名称', async ({ regionManagementPage }) => {
  76. // 首先创建一个测试省份
  77. const originalName = generateUniqueRegionName('测试省');
  78. await regionManagementPage.createProvince({
  79. name: originalName,
  80. code: generateUniqueRegionCode('PROV'),
  81. level: 1,
  82. });
  83. createdProvinces.push(originalName);
  84. // 组件会自动刷新省级数据(React Query invalidateQueries)
  85. await page.waitForTimeout(TIMEOUTS.LONG);
  86. // 编辑区域名称
  87. const newName = generateUniqueRegionName('编辑后的省');
  88. const result = await regionManagementPage.editRegion(originalName, {
  89. name: newName,
  90. });
  91. // 验证编辑成功
  92. expect(result.success).toBe(true);
  93. expect(result.hasError).toBe(false);
  94. // 等待树形结构刷新
  95. await regionManagementPage.waitForTreeLoaded();
  96. // 验证列表中显示新名称
  97. const exists = await regionManagementPage.regionExists(newName);
  98. expect(exists).toBe(true);
  99. // 更新清理列表中的名称
  100. const index = createdProvinces.indexOf(originalName);
  101. if (index > -1) {
  102. createdProvinces[index] = newName;
  103. }
  104. });
  105. test('编辑后原名称不应存在', async ({ regionManagementPage }) => {
  106. const originalName = generateUniqueRegionName('测试省');
  107. await regionManagementPage.createProvince({
  108. name: originalName,
  109. code: generateUniqueRegionCode('PROV'),
  110. level: 1,
  111. });
  112. createdProvinces.push(originalName);
  113. // 组件会自动刷新省级数据(React Query invalidateQueries)
  114. await page.waitForTimeout(TIMEOUTS.LONG);
  115. const newName = generateUniqueRegionName('编辑后的省');
  116. const result = await regionManagementPage.editRegion(originalName, { name: newName });
  117. expect(result.success).toBe(true);
  118. await regionManagementPage.waitForTreeLoaded();
  119. // 验证原名称不存在
  120. const originalExists = await regionManagementPage.regionExists(originalName);
  121. expect(originalExists).toBe(false);
  122. // 验证新名称存在
  123. const newExists = await regionManagementPage.regionExists(newName);
  124. expect(newExists).toBe(true);
  125. // 更新清理列表
  126. const index = createdProvinces.indexOf(originalName);
  127. if (index > -1) {
  128. createdProvinces[index] = newName;
  129. }
  130. });
  131. });
  132. test.describe('修改区域代码', () => {
  133. test('应该成功修改行政区划代码', async ({ regionManagementPage }) => {
  134. const provinceName = generateUniqueRegionName('测试省');
  135. await regionManagementPage.createProvince({
  136. name: provinceName,
  137. code: 'OLD_CODE',
  138. level: 1,
  139. });
  140. createdProvinces.push(provinceName);
  141. await regionManagementPage.waitForTreeLoaded();
  142. // 修改代码
  143. const newCode = generateUniqueRegionCode('NEW');
  144. const result = await regionManagementPage.editRegion(provinceName, {
  145. code: newCode,
  146. });
  147. expect(result.success).toBe(true);
  148. expect(result.hasError).toBe(false);
  149. });
  150. test('应该能同时修改名称和代码', async ({ regionManagementPage }) => {
  151. const originalName = generateUniqueRegionName('测试省');
  152. await regionManagementPage.createProvince({
  153. name: originalName,
  154. code: 'OLD_CODE',
  155. level: 1,
  156. });
  157. createdProvinces.push(originalName);
  158. await regionManagementPage.waitForTreeLoaded();
  159. const newName = generateUniqueRegionName('新省名');
  160. const newCode = generateUniqueRegionCode('NEW');
  161. const result = await regionManagementPage.editRegion(originalName, {
  162. name: newName,
  163. code: newCode,
  164. });
  165. expect(result.success).toBe(true);
  166. expect(result.hasError).toBe(false);
  167. await regionManagementPage.waitForTreeLoaded();
  168. expect(await regionManagementPage.regionExists(newName)).toBe(true);
  169. // 更新清理列表
  170. const index = createdProvinces.indexOf(originalName);
  171. if (index > -1) {
  172. createdProvinces[index] = newName;
  173. }
  174. });
  175. });
  176. test.describe('区域状态切换', () => {
  177. test('应该成功禁用已启用的区域', async ({ regionManagementPage }) => {
  178. const provinceName = generateUniqueRegionName('测试省');
  179. console.debug(`创建省份: ${provinceName}`);
  180. await regionManagementPage.createProvince({
  181. name: provinceName,
  182. code: generateUniqueRegionCode('PROV'),
  183. level: 1,
  184. });
  185. createdProvinces.push(provinceName);
  186. // 等待树形结构加载完成
  187. await regionManagementPage.waitForTreeLoaded();
  188. // 验证区域存在并获取初始状态
  189. const exists = await regionManagementPage.regionExists(provinceName);
  190. expect(exists).toBe(true);
  191. const initialStatus = await regionManagementPage.getRegionStatus(provinceName);
  192. expect(initialStatus).toBe('启用');
  193. // 禁用区域
  194. const success = await regionManagementPage.toggleRegionStatus(provinceName);
  195. expect(success).toBe(true);
  196. // 等待树形结构刷新(toggleRegionStatus 内部已等待)
  197. await regionManagementPage.waitForTreeLoaded();
  198. // 验证状态已更新
  199. const newStatus = await regionManagementPage.getRegionStatus(provinceName);
  200. expect(newStatus).toBe('禁用');
  201. });
  202. test('应该成功启用已禁用的区域', async ({ regionManagementPage }) => {
  203. const provinceName = generateUniqueRegionName('测试省');
  204. await regionManagementPage.createProvince({
  205. name: provinceName,
  206. code: generateUniqueRegionCode('PROV'),
  207. level: 1,
  208. });
  209. createdProvinces.push(provinceName);
  210. await regionManagementPage.waitForTreeLoaded();
  211. // 先禁用
  212. await regionManagementPage.toggleRegionStatus(provinceName);
  213. await regionManagementPage.waitForTreeLoaded();
  214. // 验证已禁用
  215. const disabledStatus = await regionManagementPage.getRegionStatus(provinceName);
  216. expect(disabledStatus).toBe('禁用');
  217. // 再启用
  218. const success = await regionManagementPage.toggleRegionStatus(provinceName);
  219. expect(success).toBe(true);
  220. await regionManagementPage.waitForTreeLoaded();
  221. // 验证状态已恢复为启用
  222. const status = await regionManagementPage.getRegionStatus(provinceName);
  223. expect(status).toBe('启用');
  224. });
  225. test('取消状态切换应保持原状态', async ({ regionManagementPage }) => {
  226. const provinceName = generateUniqueRegionName('测试省');
  227. await regionManagementPage.createProvince({
  228. name: provinceName,
  229. code: generateUniqueRegionCode('PROV'),
  230. level: 1,
  231. });
  232. createdProvinces.push(provinceName);
  233. await regionManagementPage.waitForTreeLoaded();
  234. const initialStatus = await regionManagementPage.getRegionStatus(provinceName);
  235. expect(initialStatus).toBe('启用');
  236. // 打开状态切换对话框但取消
  237. await regionManagementPage.openToggleStatusDialog(provinceName);
  238. await regionManagementPage.cancelToggleStatus();
  239. // 验证状态未改变(不需要刷新页面)
  240. const currentStatus = await regionManagementPage.getRegionStatus(provinceName);
  241. expect(currentStatus).toBe(initialStatus);
  242. });
  243. });
  244. test.describe.skip('编辑子区域 - TODO: 需要修复 createChildRegion 功能', () => {
  245. test('应该成功编辑市级区域名称', async ({ regionManagementPage, page }) => {
  246. const provinceName = generateUniqueRegionName('测试省');
  247. const originalCityName = generateUniqueRegionName('测试市');
  248. // 创建省和市
  249. await regionManagementPage.createProvince({
  250. name: provinceName,
  251. code: generateUniqueRegionCode('PROV'),
  252. level: 1,
  253. });
  254. createdProvinces.push(provinceName);
  255. await page.goto('/admin/areas');
  256. await regionManagementPage.waitForTreeLoaded();
  257. // 创建市后,先展开省节点验证市已创建
  258. const cityResult = await regionManagementPage.createChildRegion(provinceName, '市', {
  259. name: originalCityName,
  260. code: generateUniqueRegionCode('CITY'),
  261. level: 2,
  262. });
  263. expect(cityResult.success).toBe(true);
  264. await page.goto('/admin/areas');
  265. await regionManagementPage.waitForTreeLoaded();
  266. // 直接展开新创建的省节点(滚动到可见区域)
  267. await regionManagementPage.expandNode(provinceName);
  268. await page.waitForTimeout(TIMEOUTS.LONG);
  269. // 验证市级区域可见
  270. const cityVisible = await regionManagementPage.regionExists(originalCityName);
  271. console.debug(`市级区域 "${originalCityName}" 可见: ${cityVisible}`);
  272. expect(cityVisible).toBe(true);
  273. // 编辑城市名称
  274. const newCityName = generateUniqueRegionName('编辑后的市');
  275. const result = await regionManagementPage.editRegion(originalCityName, {
  276. name: newCityName,
  277. });
  278. expect(result.success).toBe(true);
  279. expect(result.hasError).toBe(false);
  280. });
  281. test('应该成功编辑区级区域状态', async ({ regionManagementPage, page }) => {
  282. const provinceName = generateUniqueRegionName('测试省');
  283. const cityName = generateUniqueRegionName('测试市');
  284. const districtName = generateUniqueRegionName('测试区');
  285. // 创建省市区三级结构
  286. await regionManagementPage.createProvince({
  287. name: provinceName,
  288. code: generateUniqueRegionCode('PROV'),
  289. level: 1,
  290. });
  291. createdProvinces.push(provinceName);
  292. await page.goto('/admin/areas');
  293. await regionManagementPage.waitForTreeLoaded();
  294. const cityResult = await regionManagementPage.createChildRegion(provinceName, '市', {
  295. name: cityName,
  296. code: generateUniqueRegionCode('CITY'),
  297. level: 2,
  298. });
  299. expect(cityResult.success).toBe(true);
  300. await page.goto('/admin/areas');
  301. await regionManagementPage.waitForTreeLoaded();
  302. const districtResult = await regionManagementPage.createChildRegion(provinceName, '市', {
  303. name: districtName,
  304. code: generateUniqueRegionCode('DISTRICT'),
  305. level: 3,
  306. });
  307. expect(districtResult.success).toBe(true);
  308. await page.goto('/admin/areas');
  309. await regionManagementPage.waitForTreeLoaded();
  310. // 展开省节点
  311. await regionManagementPage.expandNode(provinceName);
  312. await page.waitForTimeout(TIMEOUTS.LONG);
  313. // 切换区的状态
  314. const success = await regionManagementPage.toggleRegionStatus(districtName);
  315. expect(success).toBe(true);
  316. });
  317. });
  318. test.describe('表单验证', () => {
  319. test('清空名称时应显示错误提示', async ({ regionManagementPage }) => {
  320. const provinceName = generateUniqueRegionName('测试省');
  321. await regionManagementPage.createProvince({
  322. name: provinceName,
  323. code: generateUniqueRegionCode('PROV'),
  324. level: 1,
  325. });
  326. createdProvinces.push(provinceName);
  327. await regionManagementPage.waitForTreeLoaded();
  328. // 打开编辑对话框并清空名称
  329. await regionManagementPage.openEditDialog(provinceName);
  330. await regionManagementPage.page.getByLabel('区域名称').fill('');
  331. // 提交表单
  332. const submitButton = regionManagementPage.page.getByRole('button', { name: '更新' });
  333. await submitButton.click();
  334. // 验证错误提示 - 可能是内联错误或 toast
  335. await regionManagementPage.page.waitForTimeout(TIMEOUTS.MEDIUM);
  336. // 检查内联错误
  337. const nameError = regionManagementPage.page.getByText('区域名称不能为空');
  338. const hasInlineError = await nameError.count() > 0;
  339. // 检查 toast 错误
  340. const errorToast = regionManagementPage.page.locator('[data-sonner-toast][data-type="error"]');
  341. const hasToastError = await errorToast.count() > 0;
  342. // 至少应该有一种错误提示
  343. expect(hasInlineError || hasToastError).toBe(true);
  344. // 取消对话框
  345. await regionManagementPage.cancelDialog();
  346. });
  347. test('应该支持取消编辑操作', async ({ regionManagementPage }) => {
  348. const provinceName = generateUniqueRegionName('测试省');
  349. await regionManagementPage.createProvince({
  350. name: provinceName,
  351. code: generateUniqueRegionCode('PROV'),
  352. level: 1,
  353. });
  354. createdProvinces.push(provinceName);
  355. await regionManagementPage.waitForTreeLoaded();
  356. // 打开编辑对话框
  357. await regionManagementPage.openEditDialog(provinceName);
  358. // 修改名称
  359. const newName = generateUniqueRegionName('修改后的省');
  360. await regionManagementPage.page.getByLabel('区域名称').fill(newName);
  361. // 点击取消按钮
  362. await regionManagementPage.cancelDialog();
  363. // 验证对话框已关闭
  364. const dialog = regionManagementPage.page.locator('[role="dialog"]');
  365. await expect(dialog).not.toBeVisible();
  366. // 验证数据未修改 - 原名称仍存在
  367. await regionManagementPage.waitForTreeLoaded();
  368. const originalExists = await regionManagementPage.regionExists(provinceName);
  369. expect(originalExists).toBe(true);
  370. });
  371. });
  372. test.describe('连续编辑操作', () => {
  373. test('应该能连续编辑多个区域', async ({ regionManagementPage }) => {
  374. // 创建多个省份
  375. const province1 = generateUniqueRegionName('测试省1');
  376. const province2 = generateUniqueRegionName('测试省2');
  377. await regionManagementPage.createProvince({
  378. name: province1,
  379. code: generateUniqueRegionCode('PROV1'),
  380. level: 1,
  381. });
  382. createdProvinces.push(province1);
  383. await regionManagementPage.waitForTreeLoaded();
  384. await regionManagementPage.createProvince({
  385. name: province2,
  386. code: generateUniqueRegionCode('PROV2'),
  387. level: 1,
  388. });
  389. createdProvinces.push(province2);
  390. await regionManagementPage.waitForTreeLoaded();
  391. // 编辑第一个省份
  392. const newProvince1Name = generateUniqueRegionName('编辑后的省1');
  393. const result1 = await regionManagementPage.editRegion(province1, { name: newProvince1Name });
  394. expect(result1.success).toBe(true);
  395. // 更新清理列表
  396. const index1 = createdProvinces.indexOf(province1);
  397. if (index1 > -1) {
  398. createdProvinces[index1] = newProvince1Name;
  399. }
  400. // 编辑第二个省份
  401. const newProvince2Name = generateUniqueRegionName('编辑后的省2');
  402. const result2 = await regionManagementPage.editRegion(province2, { name: newProvince2Name });
  403. expect(result2.success).toBe(true);
  404. // 更新清理列表
  405. const index2 = createdProvinces.indexOf(province2);
  406. if (index2 > -1) {
  407. createdProvinces[index2] = newProvince2Name;
  408. }
  409. });
  410. });
  411. });