|
|
@@ -0,0 +1,378 @@
|
|
|
+import { test, expect } from '@playwright/test';
|
|
|
+import { TenantLoginPage } from '../pages/tenant/tenant-login.page';
|
|
|
+import { TenantAdvertisementPage, TenantAdvertisementTypePage } from '../pages/tenant/tenant-advertisement.page';
|
|
|
+import testAdvertisements from '../fixtures/test-advertisements.json' with { type: 'json' };
|
|
|
+
|
|
|
+/**
|
|
|
+ * E2E测试:租户后台统一广告管理UI交互
|
|
|
+ *
|
|
|
+ * 目的:验证租户后台的广告管理功能在实际浏览器环境中能够正常工作
|
|
|
+ * 覆盖:登录、导航、CRUD操作、表单验证、分页、搜索等所有交互场景
|
|
|
+ *
|
|
|
+ * ## 测试前置条件
|
|
|
+ *
|
|
|
+ * 1. 数据库中存在测试租户(tenant_id=1)
|
|
|
+ * 2. 数据库中存在测试超级管理员(username=admin, password=admin123)
|
|
|
+ * 3. 测试环境可访问(http://localhost:8080 或 E2E_BASE_URL 指定的环境)
|
|
|
+ *
|
|
|
+ * ## 测试数据准备
|
|
|
+ *
|
|
|
+ * ```sql
|
|
|
+ * -- 创建测试租户
|
|
|
+ * INSERT INTO tenant_mt (id, name, code, status, created_at, updated_at)
|
|
|
+ * VALUES (1, '测试租户', 'test-tenant', 1, NOW(), NOW());
|
|
|
+ *
|
|
|
+ * -- 创建测试超级管理员 (密码: admin123)
|
|
|
+ * INSERT INTO users_mt (id, tenant_id, username, password, registration_source, is_disabled, is_deleted, created_at, updated_at)
|
|
|
+ * VALUES (1, 1, 'admin', '$2b$10$x3t2kofPmACnk6y6lfL6ouU836LBEuZE9BinQ3ZzA4Xd04izyY42K', 'web', 0, 0, NOW(), NOW());
|
|
|
+ * ```
|
|
|
+ */
|
|
|
+
|
|
|
+// 测试配置
|
|
|
+const BASE_URL = process.env.E2E_BASE_URL || 'http://localhost:8080';
|
|
|
+const TEST_USERNAME = process.env.TEST_USERNAME || 'admin';
|
|
|
+const TEST_PASSWORD = process.env.TEST_PASSWORD || 'admin123';
|
|
|
+
|
|
|
+test.describe('租户后台统一广告管理UI交互测试', () => {
|
|
|
+ let loginPage: TenantLoginPage;
|
|
|
+ let advertisementPage: TenantAdvertisementPage;
|
|
|
+ let typePage: TenantAdvertisementTypePage;
|
|
|
+
|
|
|
+ // 每个测试前登录
|
|
|
+ test.beforeEach(async ({ page }) => {
|
|
|
+ loginPage = new TenantLoginPage(page);
|
|
|
+ advertisementPage = new TenantAdvertisementPage(page);
|
|
|
+ typePage = new TenantAdvertisementTypePage(page);
|
|
|
+
|
|
|
+ // 导航到登录页
|
|
|
+ await loginPage.goto();
|
|
|
+ await loginPage.login(TEST_USERNAME, TEST_PASSWORD);
|
|
|
+ await loginPage.expectLoginSuccess();
|
|
|
+ });
|
|
|
+
|
|
|
+ test.describe('任务2: 登录流程测试', () => {
|
|
|
+ test('应该成功登录并跳转到租户控制台', async ({ page }) => {
|
|
|
+ await expect(page).toHaveURL(/\/tenant\/dashboard/);
|
|
|
+ await expect(page.getByRole('heading', { name: /租户控制台|仪表盘|Dashboard/i })).toBeVisible();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该显示错误提示当密码错误时', async ({ page }) => {
|
|
|
+ // 登出
|
|
|
+ await page.goto('/tenant/login');
|
|
|
+ const loginPage2 = new TenantLoginPage(page);
|
|
|
+ await loginPage2.login(TEST_USERNAME, 'wrongpassword');
|
|
|
+ await loginPage2.expectLoginError();
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ test.describe('任务3: 导航测试', () => {
|
|
|
+ test('应该能够导航到广告管理页面', async ({ page }) => {
|
|
|
+ // 点击广告管理菜单项
|
|
|
+ const advertisementMenu = page.getByRole('link', { name: /广告管理/ }).or(
|
|
|
+ page.getByText('广告管理').locator('..')
|
|
|
+ );
|
|
|
+ await advertisementMenu.click();
|
|
|
+ await page.waitForLoadState('networkidle');
|
|
|
+
|
|
|
+ await expect(page).toHaveURL(/\/tenant\/unified-advertisements/);
|
|
|
+ await advertisementPage.pageTitle.toBeVisible();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该能够导航到广告类型管理页面', async ({ page }) => {
|
|
|
+ // 点击广告类型管理菜单项
|
|
|
+ const typeMenu = page.getByRole('link', { name: /广告类型管理/ }).or(
|
|
|
+ page.getByText('广告类型管理').locator('..')
|
|
|
+ );
|
|
|
+ await typeMenu.click();
|
|
|
+ await page.waitForLoadState('networkidle');
|
|
|
+
|
|
|
+ await expect(page).toHaveURL(/\/tenant\/unified-advertisement-types/);
|
|
|
+ await typePage.pageTitle.toBeVisible();
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ test.describe('任务4: 广告列表测试', () => {
|
|
|
+ test.beforeEach(async ({ page }) => {
|
|
|
+ await page.goto('/tenant/unified-advertisements');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该正确显示广告列表页面', async () => {
|
|
|
+ await expect(advertisementPage.pageTitle).toBeVisible();
|
|
|
+ await expect(advertisementPage.createButton).toBeVisible();
|
|
|
+ await expect(advertisementPage.searchInput).toBeVisible();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该正确显示广告列表数据', async () => {
|
|
|
+ const rowCount = await advertisementPage.getRowCount();
|
|
|
+ // 至少应该显示表头,即使没有数据
|
|
|
+ expect(rowCount).toBeGreaterThanOrEqual(0);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ test.describe('任务5: 创建广告测试', () => {
|
|
|
+ test.beforeEach(async ({ page }) => {
|
|
|
+ await page.goto('/tenant/unified-advertisements');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该能够打开创建对话框', async () => {
|
|
|
+ await advertisementPage.clickCreate();
|
|
|
+ await advertisementPage.expectModalVisible(true);
|
|
|
+ await expect(advertisementPage.modalTitle).toHaveText(/创建|新建/i);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该能够成功创建广告', async () => {
|
|
|
+ const timestamp = Date.now();
|
|
|
+ const testData = {
|
|
|
+ ...testAdvertisements.testAdvertisement,
|
|
|
+ title: `${testAdvertisements.testAdvertisement.title}_${timestamp}`,
|
|
|
+ code: `${testAdvertisements.testAdvertisement.code}_${timestamp}`
|
|
|
+ };
|
|
|
+
|
|
|
+ await advertisementPage.clickCreate();
|
|
|
+ await advertisementPage.fillForm(testData);
|
|
|
+ await advertisementPage.submitForm();
|
|
|
+ await advertisementPage.expectModalVisible(false);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ test.describe('任务6: 编辑广告测试', () => {
|
|
|
+ test('应该能够打开编辑对话框并更新广告', async ({ page }) => {
|
|
|
+ await page.goto('/tenant/unified-advertisements');
|
|
|
+
|
|
|
+ // 获取第一行的ID(如果存在)
|
|
|
+ const firstRow = page.locator('tbody tr').first();
|
|
|
+ const hasData = await firstRow.count() > 0;
|
|
|
+
|
|
|
+ if (hasData) {
|
|
|
+ // 查找编辑按钮
|
|
|
+ const editButton = firstRow.getByTestId(/edit-button-\d+/);
|
|
|
+ const editCount = await editButton.count();
|
|
|
+
|
|
|
+ if (editCount > 0) {
|
|
|
+ await editButton.first().click();
|
|
|
+ await advertisementPage.expectModalVisible(true);
|
|
|
+
|
|
|
+ // 修改标题
|
|
|
+ const timestamp = Date.now();
|
|
|
+ await advertisementPage.titleInput.fill(`更新标题_${timestamp}`);
|
|
|
+ await advertisementPage.submitForm();
|
|
|
+ await advertisementPage.expectModalVisible(false);
|
|
|
+ } else {
|
|
|
+ test.skip(true, '没有可编辑的广告');
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ test.skip(true, '没有广告数据');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ test.describe('任务7: 删除广告测试', () => {
|
|
|
+ test('应该能够删除广告', async ({ page }) => {
|
|
|
+ await page.goto('/tenant/unified-advertisements');
|
|
|
+
|
|
|
+ const firstRow = page.locator('tbody tr').first();
|
|
|
+ const hasData = await firstRow.count() > 0;
|
|
|
+
|
|
|
+ if (hasData) {
|
|
|
+ const deleteButton = firstRow.getByTestId(/delete-button-\d+/);
|
|
|
+ const deleteCount = await deleteButton.count();
|
|
|
+
|
|
|
+ if (deleteCount > 0) {
|
|
|
+ const rowCountBefore = await advertisementPage.getRowCount();
|
|
|
+
|
|
|
+ await deleteButton.first().click();
|
|
|
+ await advertisementPage.expectDeleteDialogVisible(true);
|
|
|
+ await advertisementPage.confirmDelete();
|
|
|
+
|
|
|
+ // 等待对话框关闭
|
|
|
+ await page.waitForTimeout(1000);
|
|
|
+
|
|
|
+ // 验证删除后数量减少(或至少对话框关闭)
|
|
|
+ await advertisementPage.expectDeleteDialogVisible(false);
|
|
|
+ } else {
|
|
|
+ test.skip(true, '没有可删除的广告');
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ test.skip(true, '没有广告数据');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该能够取消删除操作', async ({ page }) => {
|
|
|
+ await page.goto('/tenant/unified-advertisements');
|
|
|
+
|
|
|
+ const firstRow = page.locator('tbody tr').first();
|
|
|
+ const hasData = await firstRow.count() > 0;
|
|
|
+
|
|
|
+ if (hasData) {
|
|
|
+ const deleteButton = firstRow.getByTestId(/delete-button-\d+/);
|
|
|
+ const deleteCount = await deleteButton.count();
|
|
|
+
|
|
|
+ if (deleteCount > 0) {
|
|
|
+ await deleteButton.first().click();
|
|
|
+ await advertisementPage.expectDeleteDialogVisible(true);
|
|
|
+ await advertisementPage.cancelDelete();
|
|
|
+ await advertisementPage.expectDeleteDialogVisible(false);
|
|
|
+ } else {
|
|
|
+ test.skip(true, '没有可删除的广告');
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ test.skip(true, '没有广告数据');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ test.describe('任务8: 广告类型管理测试', () => {
|
|
|
+ test.beforeEach(async ({ page }) => {
|
|
|
+ await page.goto('/tenant/unified-advertisement-types');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该正确显示广告类型列表页面', async () => {
|
|
|
+ await expect(typePage.pageTitle).toBeVisible();
|
|
|
+ await expect(typePage.createButton).toBeVisible();
|
|
|
+ await expect(typePage.searchInput).toBeVisible();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该能够创建广告类型', async () => {
|
|
|
+ const timestamp = Date.now();
|
|
|
+ const testData = {
|
|
|
+ ...testAdvertisements.testAdvertisementType,
|
|
|
+ name: `${testAdvertisements.testAdvertisementType.name}_${timestamp}`,
|
|
|
+ code: `${testAdvertisements.testAdvertisementType.code}_${timestamp}`
|
|
|
+ };
|
|
|
+
|
|
|
+ await typePage.clickCreate();
|
|
|
+ await typePage.expectModalVisible(true);
|
|
|
+ await typePage.fillForm(testData);
|
|
|
+ await typePage.submitForm();
|
|
|
+ await typePage.expectModalVisible(false);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ test.describe('任务9: 分页功能测试', () => {
|
|
|
+ test('应该显示分页组件', async ({ page }) => {
|
|
|
+ await page.goto('/tenant/unified-advertisements');
|
|
|
+
|
|
|
+ // 检查是否有分页组件(取决于数据量)
|
|
|
+ const pagination = page.locator('[data-testid="pagination"]');
|
|
|
+ const hasPagination = await pagination.count() > 0;
|
|
|
+
|
|
|
+ if (hasPagination) {
|
|
|
+ await expect(pagination).toBeVisible();
|
|
|
+ } else {
|
|
|
+ test.skip(true, '数据量不足,无分页组件');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ test.describe('任务10: 搜索功能测试', () => {
|
|
|
+ test('应该能够按标题搜索广告', async ({ page }) => {
|
|
|
+ await page.goto('/tenant/unified-advertisements');
|
|
|
+
|
|
|
+ // 输入搜索关键词
|
|
|
+ await advertisementPage.search('测试');
|
|
|
+ await page.waitForTimeout(1000);
|
|
|
+
|
|
|
+ // 搜索应该执行(可能没有结果,但不会出错)
|
|
|
+ const searchValue = await advertisementPage.searchInput.inputValue();
|
|
|
+ expect(searchValue).toBe('测试');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该能够清空搜索', async ({ page }) => {
|
|
|
+ await page.goto('/tenant/unified-advertisements');
|
|
|
+
|
|
|
+ await advertisementPage.search('测试');
|
|
|
+ await page.waitForTimeout(500);
|
|
|
+
|
|
|
+ await advertisementPage.searchInput.clear();
|
|
|
+ await page.waitForTimeout(500);
|
|
|
+
|
|
|
+ const searchValue = await advertisementPage.searchInput.inputValue();
|
|
|
+ expect(searchValue).toBe('');
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ test.describe('任务11: 表单验证测试', () => {
|
|
|
+ test.beforeEach(async ({ page }) => {
|
|
|
+ await page.goto('/tenant/unified-advertisements');
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该验证必填字段', async () => {
|
|
|
+ await advertisementPage.clickCreate();
|
|
|
+
|
|
|
+ // 尝试提交空表单
|
|
|
+ await advertisementPage.submitForm();
|
|
|
+
|
|
|
+ // 应该显示验证错误(具体错误信息取决于UI实现)
|
|
|
+ await page.waitForTimeout(500);
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该要求输入广告标题', async () => {
|
|
|
+ await advertisementPage.clickCreate();
|
|
|
+
|
|
|
+ // 不填写标题,直接提交
|
|
|
+ await advertisementPage.submitForm();
|
|
|
+
|
|
|
+ // 标题输入框应该显示错误状态
|
|
|
+ await page.waitForTimeout(500);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ test.describe('任务13: 响应式布局测试', () => {
|
|
|
+ test('应该在桌面视图正常显示', async ({ page }) => {
|
|
|
+ // 设置桌面视口
|
|
|
+ await page.setViewportSize({ width: 1920, height: 1080 });
|
|
|
+ await page.goto('/tenant/unified-advertisements');
|
|
|
+
|
|
|
+ await expect(advertisementPage.pageTitle).toBeVisible();
|
|
|
+ await expect(advertisementPage.createButton).toBeVisible();
|
|
|
+ });
|
|
|
+
|
|
|
+ test('应该在移动端视图正常显示', async ({ page }) => {
|
|
|
+ // 设置移动端视口
|
|
|
+ await page.setViewportSize({ width: 375, height: 667 });
|
|
|
+ await page.goto('/tenant/unified-advertisements');
|
|
|
+
|
|
|
+ await expect(advertisementPage.pageTitle).toBeVisible();
|
|
|
+
|
|
|
+ // 移动端可能需要点击菜单按钮
|
|
|
+ const menuButton = page.getByTestId('mobile-menu-button');
|
|
|
+ const hasMenuButton = await menuButton.count() > 0;
|
|
|
+
|
|
|
+ if (hasMenuButton) {
|
|
|
+ await expect(menuButton).toBeVisible();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ test.describe('完整用户流程测试', () => {
|
|
|
+ test('应该完成完整的广告CRUD流程', async ({ page }) => {
|
|
|
+ const timestamp = Date.now();
|
|
|
+
|
|
|
+ // 1. 导航到广告管理
|
|
|
+ await page.goto('/tenant/unified-advertisements');
|
|
|
+ await expect(page).toHaveURL(/\/tenant\/unified-advertisements/);
|
|
|
+
|
|
|
+ // 2. 创建新广告
|
|
|
+ const testData = {
|
|
|
+ ...testAdvertisements.testAdvertisement,
|
|
|
+ title: `完整流程测试_${timestamp}`,
|
|
|
+ code: `COMPLETE_FLOW_${timestamp}`
|
|
|
+ };
|
|
|
+
|
|
|
+ await advertisementPage.clickCreate();
|
|
|
+ await advertisementPage.expectModalVisible(true);
|
|
|
+ await advertisementPage.fillForm(testData);
|
|
|
+ await advertisementPage.submitForm();
|
|
|
+ await advertisementPage.expectModalVisible(false);
|
|
|
+
|
|
|
+ // 3. 验证广告创建成功(搜索刚创建的广告)
|
|
|
+ await page.waitForTimeout(1000);
|
|
|
+ await advertisementPage.search(testData.title);
|
|
|
+ await page.waitForTimeout(1000);
|
|
|
+
|
|
|
+ // 4. 清空搜索
|
|
|
+ await advertisementPage.searchInput.clear();
|
|
|
+ await page.waitForTimeout(500);
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|