feat: 重建 Bell GoAdmin 产品骨架 (#62)
This commit is contained in:
@@ -0,0 +1,499 @@
|
||||
/**
|
||||
* go-admin-ui Navigation and Content Rendering Validation
|
||||
*
|
||||
* Verifies the following fixes:
|
||||
* 1. permission.js loadView: require.context now extracts .default (fixes component loading)
|
||||
* 2. main.js: registers both `this.getDicts` and `this.$getDicts` (fixes 107 usage sites)
|
||||
* 3. main.js: registers both `Pagination` and `AppPagination` component names
|
||||
*
|
||||
* Auth: JWT token pre-obtained via captcha-solving, injected via context.addCookies()
|
||||
* before page navigation, so Vue router guard reads the cookie correctly.
|
||||
*/
|
||||
|
||||
import { test, expect, Page, BrowserContext } from '@playwright/test';
|
||||
|
||||
const BASE_URL = 'http://localhost:9527';
|
||||
|
||||
// Set via an ephemeral test account and environment variable when running the test.
|
||||
const ADMIN_TOKEN = process.env.BELL_E2E_TOKEN || '';
|
||||
|
||||
// Setup authenticated browser context by adding cookie before first navigation
|
||||
async function setupAuth(context: BrowserContext): Promise<boolean> {
|
||||
if (!ADMIN_TOKEN) {
|
||||
console.log('No ADMIN_TOKEN provided');
|
||||
return false;
|
||||
}
|
||||
|
||||
await context.addCookies([{
|
||||
name: 'Bell-Admin-Token',
|
||||
value: ADMIN_TOKEN,
|
||||
domain: 'localhost',
|
||||
path: '/',
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
}]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
test.describe('go-admin-ui Navigation and Content Rendering Validation', () => {
|
||||
|
||||
test('01 - Initial page load redirects to login', async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push(msg.text());
|
||||
});
|
||||
|
||||
await page.goto(BASE_URL, { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const url = page.url();
|
||||
const title = await page.title();
|
||||
const bodyLen = (await page.locator('body').innerHTML()).length;
|
||||
|
||||
console.log('URL:', url);
|
||||
console.log('Title:', title);
|
||||
console.log('Body length:', bodyLen, 'chars');
|
||||
|
||||
expect(url).toContain('login');
|
||||
expect(title).toContain('Bell');
|
||||
expect(bodyLen).toBeGreaterThan(10000);
|
||||
|
||||
const jsErrors = errors.filter(e =>
|
||||
!e.includes('net::ERR') &&
|
||||
!e.includes('Failed to load resource') &&
|
||||
!e.includes('ERR_CERT')
|
||||
);
|
||||
console.log('JS errors on login page:', jsErrors.length);
|
||||
jsErrors.forEach((e, i) => console.log(` ${i + 1}. ${e}`));
|
||||
|
||||
await page.screenshot({ path: 'test-results/01-initial-load.png', fullPage: true });
|
||||
console.log('Screenshot: test-results/01-initial-load.png');
|
||||
});
|
||||
|
||||
test('02 - Login page form elements and captcha rendering', async ({ page }) => {
|
||||
await page.goto(BASE_URL, { waitUntil: 'networkidle' });
|
||||
await page.waitForURL(/login/, { timeout: 10000 });
|
||||
|
||||
// Note: both username and captcha inputs have name="username" (bug in source)
|
||||
const usernameInput = page.locator('input[placeholder="用户名"]');
|
||||
const passwordInput = page.locator('input[name="password"]');
|
||||
const codeInput = page.locator('input[placeholder="验证码"]');
|
||||
const loginBtn = page.locator('button').filter({ hasText: /登/ }).first();
|
||||
const captchaImg = page.locator('.login-code img');
|
||||
|
||||
await expect(usernameInput).toBeVisible({ timeout: 5000 });
|
||||
await expect(passwordInput).toBeVisible({ timeout: 5000 });
|
||||
await expect(codeInput).toBeVisible({ timeout: 5000 });
|
||||
await expect(loginBtn).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const captchaCount = await captchaImg.count();
|
||||
console.log('Captcha image count:', captchaCount);
|
||||
if (captchaCount > 0) {
|
||||
const src = await captchaImg.getAttribute('src');
|
||||
const isBase64 = src?.startsWith('data:image/');
|
||||
console.log('Captcha rendered as base64:', isBase64);
|
||||
expect(isBase64).toBeTruthy();
|
||||
}
|
||||
|
||||
console.log('PASS: All login form elements present and functional');
|
||||
await page.screenshot({ path: 'test-results/02-login-form.png', fullPage: true });
|
||||
});
|
||||
|
||||
test('03 - Dashboard layout (with auth token)', async ({ page, context }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push(msg.text());
|
||||
});
|
||||
|
||||
const authOk = await setupAuth(context);
|
||||
if (!authOk) {
|
||||
console.log('SKIP: No ADMIN_TOKEN provided');
|
||||
console.log('Run with: ADMIN_TOKEN=<token> npx playwright test');
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate directly to dashboard (cookie was set before this)
|
||||
await page.goto(`${BASE_URL}/#/dashboard`, { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const url = page.url();
|
||||
console.log('Dashboard URL:', url);
|
||||
const isDashboard = url.includes('dashboard') || (!url.includes('login'));
|
||||
console.log('On dashboard (not login):', isDashboard);
|
||||
|
||||
// Check layout elements
|
||||
const hasSidebar = await page.locator('.sidebar-container').count() > 0;
|
||||
const hasNavbar = await page.locator('.navbar').count() > 0;
|
||||
const hasAppMain = await page.locator('.app-main').count() > 0;
|
||||
const hasTagsView = await page.locator('.tags-view-container').count() > 0;
|
||||
|
||||
console.log('=== Dashboard Layout ===');
|
||||
console.log('Sidebar (.sidebar-container):', hasSidebar);
|
||||
console.log('Navbar (.navbar):', hasNavbar);
|
||||
console.log('App main (.app-main):', hasAppMain);
|
||||
console.log('Tags view:', hasTagsView);
|
||||
|
||||
await page.screenshot({ path: 'test-results/03-dashboard.png', fullPage: true });
|
||||
console.log('Screenshot: test-results/03-dashboard.png');
|
||||
|
||||
expect(isDashboard).toBeTruthy();
|
||||
expect(hasSidebar).toBeTruthy();
|
||||
|
||||
const jsErrors = errors.filter(e =>
|
||||
!e.includes('net::ERR') && !e.includes('Failed to load resource') && !e.includes('ERR_CERT')
|
||||
);
|
||||
if (jsErrors.length > 0) {
|
||||
console.log('JS errors on dashboard:', jsErrors);
|
||||
}
|
||||
});
|
||||
|
||||
test('04 - System management menu expansion', async ({ page, context }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push(msg.text());
|
||||
});
|
||||
|
||||
const authOk = await setupAuth(context);
|
||||
if (!authOk) {
|
||||
console.log('SKIP: No ADMIN_TOKEN provided');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${BASE_URL}/#/dashboard`, { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// List all menu items
|
||||
const menuItems = await page.locator('.el-menu-item, .el-sub-menu__title').allTextContents();
|
||||
console.log('Menu items found:', menuItems.length);
|
||||
console.log('Menu items:', menuItems.slice(0, 15).map(t => t.trim()).filter(t => t));
|
||||
|
||||
// Find and click 系统管理
|
||||
const sysMenu = page.locator('.el-sub-menu__title').filter({ hasText: '系统管理' }).first();
|
||||
const sysMenuCount = await sysMenu.count();
|
||||
console.log('系统管理 sub-menu found:', sysMenuCount > 0);
|
||||
|
||||
if (sysMenuCount > 0) {
|
||||
await sysMenu.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const subItems = await page.locator('.el-menu-item').allTextContents();
|
||||
console.log('Sub-menu items after expansion:', subItems.slice(0, 10).map(t => t.trim()).filter(t => t));
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'test-results/04-system-menu-expanded.png', fullPage: true });
|
||||
console.log('Screenshot: test-results/04-system-menu-expanded.png');
|
||||
|
||||
const jsErrors = errors.filter(e =>
|
||||
!e.includes('net::ERR') && !e.includes('Failed to load resource') && !e.includes('ERR_CERT')
|
||||
);
|
||||
if (jsErrors.length > 0) {
|
||||
console.log('JS errors:', jsErrors);
|
||||
}
|
||||
});
|
||||
|
||||
test('05 - User management page: content and fix verification', async ({ page, context }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push(msg.text());
|
||||
});
|
||||
|
||||
const authOk = await setupAuth(context);
|
||||
if (!authOk) {
|
||||
console.log('SKIP: No ADMIN_TOKEN provided');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${BASE_URL}/#/dashboard`, { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Navigate through sidebar menu
|
||||
const sysMenu = page.locator('.el-sub-menu__title').filter({ hasText: '系统管理' }).first();
|
||||
if (await sysMenu.count() > 0) {
|
||||
await sysMenu.click();
|
||||
await page.waitForTimeout(800);
|
||||
}
|
||||
|
||||
const userMenu = page.locator('.el-menu-item').filter({ hasText: '用户管理' }).first();
|
||||
const userMenuCount = await userMenu.count();
|
||||
console.log('用户管理 menu item found:', userMenuCount > 0);
|
||||
|
||||
if (userMenuCount > 0) {
|
||||
await userMenu.click();
|
||||
} else {
|
||||
await page.goto(`${BASE_URL}/#/admin/sys-user`, { waitUntil: 'networkidle' });
|
||||
}
|
||||
|
||||
// Wait 3 seconds as specified in the task
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const url = page.url();
|
||||
const hasCorrectUrl = url.includes('sys-user');
|
||||
const elCardCount = await page.locator('.el-card').count();
|
||||
const elTableCount = await page.locator('.el-table').count();
|
||||
const elTableRowCount = await page.locator('.el-table__body tr').count();
|
||||
const loadingVisible = await page.locator('.el-loading-mask:visible').count() > 0;
|
||||
|
||||
// Check for red error overlays
|
||||
const errorMsgCount = await page.locator('.el-message--error:visible').count();
|
||||
|
||||
console.log('=== User Management Page ===');
|
||||
console.log('URL:', url);
|
||||
console.log('URL contains sys-user:', hasCorrectUrl);
|
||||
console.log('el-card count:', elCardCount);
|
||||
console.log('el-table count:', elTableCount);
|
||||
console.log('el-table rows:', elTableRowCount);
|
||||
console.log('Loading spinner visible:', loadingVisible);
|
||||
console.log('Error messages visible:', errorMsgCount);
|
||||
|
||||
await page.screenshot({ path: 'test-results/05-user-management.png', fullPage: true });
|
||||
console.log('Screenshot: test-results/05-user-management.png');
|
||||
|
||||
// Fix verification - check for specific error patterns
|
||||
const criticalErrors = errors.filter(e =>
|
||||
e.includes('Cannot find module') ||
|
||||
e.includes('getDicts is not a function') ||
|
||||
e.includes('this.getDicts') ||
|
||||
e.includes('is not a function') ||
|
||||
e.includes('Cannot read properties of undefined')
|
||||
);
|
||||
|
||||
const allJsErrors = errors.filter(e =>
|
||||
!e.includes('net::ERR') && !e.includes('Failed to load resource') && !e.includes('ERR_CERT')
|
||||
);
|
||||
|
||||
console.log('\n=== Fix Verification - User Management ===');
|
||||
const fix1Pass = !criticalErrors.some(e => e.includes('Cannot find module'));
|
||||
const fix2Pass = !criticalErrors.some(e => e.includes('getDicts'));
|
||||
const fix3Pass = elTableCount > 0 || elCardCount > 0; // component rendered
|
||||
|
||||
console.log('Fix 1 (loadView .default - no "Cannot find module"):', fix1Pass ? 'PASS' : 'FAIL');
|
||||
console.log('Fix 2 (getDicts registration - no getDicts errors):', fix2Pass ? 'PASS' : 'FAIL');
|
||||
console.log('Fix 3 (Pagination/AppPagination - page renders):', fix3Pass ? 'PASS' : 'FAIL');
|
||||
console.log('el-card present:', elCardCount > 0 ? 'PASS' : 'FAIL');
|
||||
console.log('el-table present:', elTableCount > 0 ? 'PASS' : 'FAIL');
|
||||
console.log('Data loaded (rows > 0):', elTableRowCount > 0 ? 'YES' : 'NO (may be empty or still loading)');
|
||||
|
||||
if (criticalErrors.length > 0) {
|
||||
console.log('\nCRITICAL JS ERRORS:');
|
||||
criticalErrors.forEach((e, i) => console.log(` ${i + 1}. ${e}`));
|
||||
}
|
||||
|
||||
if (allJsErrors.length > 0) {
|
||||
console.log('\nAll JS errors:');
|
||||
allJsErrors.forEach((e, i) => console.log(` ${i + 1}. ${e}`));
|
||||
} else {
|
||||
console.log('\nNo JS errors on user management page');
|
||||
}
|
||||
|
||||
// Assertions
|
||||
expect(hasCorrectUrl || url.includes('admin')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('06 - Role management page: content and fix verification', async ({ page, context }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push(msg.text());
|
||||
});
|
||||
|
||||
const authOk = await setupAuth(context);
|
||||
if (!authOk) {
|
||||
console.log('SKIP: No ADMIN_TOKEN provided');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${BASE_URL}/#/dashboard`, { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Navigate to role management
|
||||
const sysMenu = page.locator('.el-sub-menu__title').filter({ hasText: '系统管理' }).first();
|
||||
if (await sysMenu.count() > 0) {
|
||||
await sysMenu.click();
|
||||
await page.waitForTimeout(800);
|
||||
}
|
||||
|
||||
const roleMenu = page.locator('.el-menu-item').filter({ hasText: '角色管理' }).first();
|
||||
const roleMenuCount = await roleMenu.count();
|
||||
console.log('角色管理 menu item found:', roleMenuCount > 0);
|
||||
|
||||
if (roleMenuCount > 0) {
|
||||
await roleMenu.click();
|
||||
} else {
|
||||
await page.goto(`${BASE_URL}/#/admin/sys-role`, { waitUntil: 'networkidle' });
|
||||
}
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const url = page.url();
|
||||
const elCardCount = await page.locator('.el-card').count();
|
||||
const elTableCount = await page.locator('.el-table').count();
|
||||
const elTableRowCount = await page.locator('.el-table__body tr').count();
|
||||
|
||||
console.log('=== Role Management Page ===');
|
||||
console.log('URL:', url);
|
||||
console.log('el-card count:', elCardCount);
|
||||
console.log('el-table count:', elTableCount);
|
||||
console.log('el-table rows:', elTableRowCount);
|
||||
|
||||
await page.screenshot({ path: 'test-results/06-role-management.png', fullPage: true });
|
||||
console.log('Screenshot: test-results/06-role-management.png');
|
||||
|
||||
const criticalErrors = errors.filter(e =>
|
||||
e.includes('Cannot find module') ||
|
||||
e.includes('getDicts is not a function') ||
|
||||
e.includes('is not a function')
|
||||
);
|
||||
|
||||
const allJsErrors = errors.filter(e =>
|
||||
!e.includes('net::ERR') && !e.includes('Failed to load resource') && !e.includes('ERR_CERT')
|
||||
);
|
||||
|
||||
console.log('el-card present:', elCardCount > 0 ? 'PASS' : 'FAIL');
|
||||
console.log('el-table present:', elTableCount > 0 ? 'PASS' : 'FAIL');
|
||||
|
||||
if (criticalErrors.length > 0) {
|
||||
console.log('CRITICAL ERRORS:', criticalErrors);
|
||||
} else {
|
||||
console.log('No critical JS errors on role management page');
|
||||
}
|
||||
|
||||
if (allJsErrors.length > 0) {
|
||||
console.log('All JS errors:', allJsErrors);
|
||||
}
|
||||
});
|
||||
|
||||
test('07 - Return to dashboard via 首页 menu', async ({ page, context }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') errors.push(msg.text());
|
||||
});
|
||||
|
||||
const authOk = await setupAuth(context);
|
||||
if (!authOk) {
|
||||
console.log('SKIP: No ADMIN_TOKEN provided');
|
||||
return;
|
||||
}
|
||||
|
||||
await page.goto(`${BASE_URL}/#/dashboard`, { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Navigate to role management first
|
||||
await page.goto(`${BASE_URL}/#/admin/sys-role`, { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(1500);
|
||||
console.log('On role management:', page.url());
|
||||
|
||||
// Click 首页 in sidebar
|
||||
const homeMenu = page.locator('.el-menu-item').filter({ hasText: '首页' }).first();
|
||||
const homeCount = await homeMenu.count();
|
||||
console.log('首页 menu item found:', homeCount > 0);
|
||||
|
||||
if (homeCount > 0) {
|
||||
await homeMenu.click();
|
||||
await page.waitForTimeout(2000);
|
||||
} else {
|
||||
await page.goto(`${BASE_URL}/#/dashboard`, { waitUntil: 'networkidle' });
|
||||
}
|
||||
|
||||
const url = page.url();
|
||||
const isDashboard = url.includes('dashboard') || url.includes('index') || !url.includes('sys-');
|
||||
console.log('URL after returning home:', url);
|
||||
console.log('On dashboard:', isDashboard);
|
||||
|
||||
await page.screenshot({ path: 'test-results/07-back-to-dashboard.png', fullPage: true });
|
||||
console.log('Screenshot: test-results/07-back-to-dashboard.png');
|
||||
|
||||
const allJsErrors = errors.filter(e =>
|
||||
!e.includes('net::ERR') && !e.includes('Failed to load resource') && !e.includes('ERR_CERT')
|
||||
);
|
||||
if (allJsErrors.length > 0) {
|
||||
console.log('JS errors:', allJsErrors);
|
||||
}
|
||||
});
|
||||
|
||||
test('08 - Complete console error analysis (all pages)', async ({ page, context }) => {
|
||||
const loginErrors: string[] = [];
|
||||
const dashboardErrors: string[] = [];
|
||||
const userMgmtErrors: string[] = [];
|
||||
|
||||
// Collect login page errors first
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') loginErrors.push(msg.text());
|
||||
});
|
||||
|
||||
await page.goto(BASE_URL, { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const jsLoginErrors = loginErrors.filter(e =>
|
||||
!e.includes('net::ERR') && !e.includes('Failed to load resource') && !e.includes('ERR_CERT')
|
||||
);
|
||||
|
||||
// Collect dashboard + user management errors if we have auth
|
||||
const authOk = await setupAuth(context);
|
||||
if (authOk) {
|
||||
page.removeAllListeners('console');
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') dashboardErrors.push(msg.text());
|
||||
});
|
||||
|
||||
await page.goto(`${BASE_URL}/#/dashboard`, { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
page.removeAllListeners('console');
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') userMgmtErrors.push(msg.text());
|
||||
});
|
||||
|
||||
await page.goto(`${BASE_URL}/#/admin/sys-user`, { waitUntil: 'networkidle' });
|
||||
await page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
const jsDashboardErrors = dashboardErrors.filter(e =>
|
||||
!e.includes('net::ERR') && !e.includes('Failed to load resource') && !e.includes('ERR_CERT')
|
||||
);
|
||||
const jsUserMgmtErrors = userMgmtErrors.filter(e =>
|
||||
!e.includes('net::ERR') && !e.includes('Failed to load resource') && !e.includes('ERR_CERT')
|
||||
);
|
||||
|
||||
const allErrors = [...jsLoginErrors, ...jsDashboardErrors, ...jsUserMgmtErrors];
|
||||
|
||||
console.log('=== Complete Console Error Analysis ===\n');
|
||||
|
||||
console.log('Login Page JS errors:', jsLoginErrors.length);
|
||||
jsLoginErrors.forEach((e, i) => console.log(` ${i + 1}. ${e}`));
|
||||
|
||||
console.log('\nDashboard JS errors:', jsDashboardErrors.length);
|
||||
jsDashboardErrors.forEach((e, i) => console.log(` ${i + 1}. ${e}`));
|
||||
|
||||
console.log('\nUser Management JS errors:', jsUserMgmtErrors.length);
|
||||
jsUserMgmtErrors.forEach((e, i) => console.log(` ${i + 1}. ${e}`));
|
||||
|
||||
// Fix-specific verification
|
||||
const hasModuleError = allErrors.some(e => e.includes('Cannot find module'));
|
||||
const hasGetDictsError = allErrors.some(e =>
|
||||
e.includes('getDicts is not a function') ||
|
||||
(e.includes('getDicts') && e.includes('not a function'))
|
||||
);
|
||||
const hasPaginationError = allErrors.some(e =>
|
||||
e.toLowerCase().includes('pagination') ||
|
||||
e.includes('AppPagination')
|
||||
);
|
||||
|
||||
console.log('\n=== Fix Verification Summary ===');
|
||||
console.log('Fix 1 - loadView .default (no "Cannot find module"):', hasModuleError ? 'FAIL' : 'PASS');
|
||||
console.log('Fix 2 - getDicts registration (no getDicts errors):', hasGetDictsError ? 'FAIL' : 'PASS');
|
||||
console.log('Fix 3 - Pagination component (no Pagination errors):', hasPaginationError ? 'FAIL' : 'PASS');
|
||||
console.log('\nTotal JS errors across all pages:', allErrors.length);
|
||||
|
||||
if (allErrors.length === 0) {
|
||||
console.log('No JS errors detected on any page!');
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'test-results/08-error-analysis-final.png', fullPage: true });
|
||||
console.log('Screenshot: test-results/08-error-analysis-final.png');
|
||||
|
||||
// All three fixes should pass
|
||||
expect(hasModuleError).toBeFalsy();
|
||||
expect(hasGetDictsError).toBeFalsy();
|
||||
expect(hasPaginationError).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,457 @@
|
||||
import { test, expect, Page, BrowserContext, ConsoleMessage } from '@playwright/test';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as http from 'http';
|
||||
|
||||
const SCREENSHOT_DIR = '/tmp/go-admin-screenshots';
|
||||
const BASE_URL = 'http://localhost:9527';
|
||||
const API_URL = 'http://localhost:8001';
|
||||
|
||||
function ensureScreenshotDir() {
|
||||
if (!fs.existsSync(SCREENSHOT_DIR)) {
|
||||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function saveScreenshot(page: Page, name: string) {
|
||||
ensureScreenshotDir();
|
||||
const filePath = path.join(SCREENSHOT_DIR, `${name}.png`);
|
||||
await page.screenshot({ path: filePath, fullPage: false });
|
||||
console.log(`[SCREENSHOT] ${filePath}`);
|
||||
}
|
||||
|
||||
function httpGet(url: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const urlObj = new URL(url);
|
||||
const req = http.request({
|
||||
hostname: urlObj.hostname,
|
||||
port: parseInt(urlObj.port),
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method: 'GET',
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: Buffer) => data += chunk);
|
||||
res.on('end', () => {
|
||||
try { resolve(JSON.parse(data)); }
|
||||
catch (e) { resolve({ raw: data }); }
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function httpPost(url: string, body: object): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const bodyStr = JSON.stringify(body);
|
||||
const urlObj = new URL(url);
|
||||
const req = http.request({
|
||||
hostname: urlObj.hostname,
|
||||
port: parseInt(urlObj.port),
|
||||
path: urlObj.pathname,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(bodyStr),
|
||||
},
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: Buffer) => data += chunk);
|
||||
res.on('end', () => {
|
||||
try { resolve(JSON.parse(data)); }
|
||||
catch (e) { resolve({ raw: data }); }
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.write(bodyStr);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function getCaptcha(): Promise<{ id: string; imageBase64: string }> {
|
||||
const resp = await httpGet(`${API_URL}/api/v1/captcha`);
|
||||
return { id: resp.id, imageBase64: resp.data };
|
||||
}
|
||||
|
||||
// 将验证码 base64 保存为图片文件并返回路径(供人工/Vision 识别)
|
||||
async function saveCaptchaImage(base64: string, filePath: string) {
|
||||
const data = base64.replace(/^data:image\/\w+;base64,/, '');
|
||||
fs.writeFileSync(filePath, Buffer.from(data, 'base64'));
|
||||
}
|
||||
|
||||
async function loginViaAPI(captchaCode: string, captchaId: string): Promise<string | null> {
|
||||
const username = process.env.BELL_E2E_USERNAME;
|
||||
const password = process.env.BELL_E2E_PASSWORD;
|
||||
if (!username || !password) {
|
||||
throw new Error('BELL_E2E_USERNAME and BELL_E2E_PASSWORD are required');
|
||||
}
|
||||
const resp = await httpPost(`${API_URL}/api/v1/login`, {
|
||||
username,
|
||||
password,
|
||||
code: captchaCode,
|
||||
uuid: captchaId,
|
||||
});
|
||||
return resp.token || null;
|
||||
}
|
||||
|
||||
async function injectToken(context: BrowserContext, token: string) {
|
||||
await context.addCookies([{
|
||||
name: 'Bell-Admin-Token',
|
||||
value: token,
|
||||
domain: 'localhost',
|
||||
path: '/',
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
sameSite: 'Lax',
|
||||
}]);
|
||||
console.log('[AUTH] Token 已注入到浏览器 Cookie');
|
||||
}
|
||||
|
||||
test.describe('侧边栏菜单导航功能验证', () => {
|
||||
const jsErrors: string[] = [];
|
||||
const jsWarnings: string[] = [];
|
||||
|
||||
test('完整导航流程验证', async ({ page, context }) => {
|
||||
ensureScreenshotDir();
|
||||
|
||||
// 收集控制台消息
|
||||
page.on('console', (msg: ConsoleMessage) => {
|
||||
const type = msg.type();
|
||||
const text = msg.text();
|
||||
|
||||
if (type === 'error') {
|
||||
// 忽略已知的网络相关错误(图片加载、WebSocket 等)
|
||||
if (!text.includes('Failed to load resource') && !text.includes('WebSocket')) {
|
||||
jsErrors.push(text);
|
||||
}
|
||||
console.log(`[CONSOLE ERROR] ${text}`);
|
||||
}
|
||||
if (type === 'warn') {
|
||||
if (text.includes('handleMouseleave') ||
|
||||
text.includes('Failed to resolve component') ||
|
||||
text.includes('is not a function')) {
|
||||
jsWarnings.push(text);
|
||||
console.log(`[CONSOLE WARN] ${text}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
page.on('pageerror', (error: Error) => {
|
||||
jsErrors.push(`[PAGE ERROR] ${error.message}`);
|
||||
console.log('[PAGE ERROR]', error.message);
|
||||
});
|
||||
|
||||
// ==================== 阶段 1:获取 Token 并注入 ====================
|
||||
console.log('\n=== 阶段 1:API 登录获取 Token ===');
|
||||
|
||||
// 获取验证码
|
||||
const captcha = await getCaptcha();
|
||||
const captchaImagePath = path.join(SCREENSHOT_DIR, 'captcha_for_login.png');
|
||||
await saveCaptchaImage(captcha.imageBase64, captchaImagePath);
|
||||
console.log(`验证码 ID: ${captcha.id}`);
|
||||
console.log(`验证码图片: ${captchaImagePath}`);
|
||||
|
||||
// 读取验证码图片进行识别(通过文件内容方式)
|
||||
// 使用已知密码组合和当前验证码
|
||||
// 注意:验证码有效期 600 秒,我们用 Node.js 直接在同进程中识别
|
||||
// 这里需要人工传入验证码,或者通过其他手段
|
||||
|
||||
// 方案:先用之前已保存的 Token(如果存在且有效)
|
||||
let token: string | null = null;
|
||||
|
||||
const savedTokenPath = '/tmp/auth_token.txt';
|
||||
if (fs.existsSync(savedTokenPath)) {
|
||||
const savedToken = fs.readFileSync(savedTokenPath, 'utf-8').trim();
|
||||
if (savedToken && savedToken.length > 20) {
|
||||
token = savedToken;
|
||||
console.log('[AUTH] 使用已保存的 Token');
|
||||
}
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
throw new Error('没有可用的 Token,请先通过 API 登录获取 token');
|
||||
}
|
||||
|
||||
// 注入 token 到浏览器 context
|
||||
await injectToken(context, token);
|
||||
|
||||
// ==================== 步骤 1:打开应用首页 ====================
|
||||
console.log('\n=== 步骤 1:打开应用首页 ===');
|
||||
await page.goto(`${BASE_URL}/#/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(3000);
|
||||
await saveScreenshot(page, '01-initial-state');
|
||||
console.log('当前 URL:', page.url());
|
||||
|
||||
// 检查是否还在登录页(token 可能无效)
|
||||
if (page.url().includes('/login')) {
|
||||
console.log('[WARN] 仍在登录页,Token 可能已过期');
|
||||
// 这里不抛异常,继续记录状态
|
||||
}
|
||||
|
||||
// ==================== 步骤 2:验证左侧菜单 ====================
|
||||
console.log('\n=== 步骤 2:检查左侧菜单 ===');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 等待侧边栏加载
|
||||
try {
|
||||
await page.waitForSelector('.el-menu', { timeout: 5000 });
|
||||
console.log('[OK] el-menu 已加载');
|
||||
} catch (e) {
|
||||
console.log('[WARN] 等待 .el-menu 超时');
|
||||
}
|
||||
|
||||
// 收集菜单信息
|
||||
const menuInfo = await page.evaluate(() => {
|
||||
const sidebarEl = document.querySelector('.sidebar-container, .el-aside, aside, .scrollbar-wrapper');
|
||||
const menuEl = document.querySelector('.el-menu');
|
||||
const menuItems = document.querySelectorAll('.el-menu-item');
|
||||
const subMenus = document.querySelectorAll('.el-sub-menu');
|
||||
const subMenuTitles = document.querySelectorAll('.el-sub-menu__title');
|
||||
|
||||
const texts: string[] = [];
|
||||
menuItems.forEach((el) => {
|
||||
const text = el.textContent?.trim();
|
||||
if (text) texts.push(text);
|
||||
});
|
||||
const subTexts: string[] = [];
|
||||
subMenuTitles.forEach((el) => {
|
||||
const text = el.textContent?.trim();
|
||||
if (text) subTexts.push(text);
|
||||
});
|
||||
|
||||
return {
|
||||
hasSidebar: !!sidebarEl,
|
||||
hasMenu: !!menuEl,
|
||||
menuItemCount: menuItems.length,
|
||||
subMenuCount: subMenus.length,
|
||||
menuItemTexts: texts,
|
||||
subMenuTexts: subTexts,
|
||||
};
|
||||
});
|
||||
|
||||
console.log('侧边栏存在:', menuInfo.hasSidebar);
|
||||
console.log('el-menu 存在:', menuInfo.hasMenu);
|
||||
console.log('菜单项数量:', menuInfo.menuItemCount);
|
||||
console.log('子菜单数量:', menuInfo.subMenuCount);
|
||||
console.log('菜单项文字:', JSON.stringify(menuInfo.menuItemTexts));
|
||||
console.log('子菜单标题:', JSON.stringify(menuInfo.subMenuTexts));
|
||||
|
||||
await saveScreenshot(page, '02-sidebar-loaded');
|
||||
|
||||
// ==================== 步骤 3:展开系统管理子菜单 ====================
|
||||
console.log('\n=== 步骤 3:展开"系统管理"子菜单 ===');
|
||||
|
||||
let sysMenuFound = false;
|
||||
// 先找精确匹配
|
||||
const sysMenuSelectors = [
|
||||
'.el-sub-menu__title:has-text("系统管理")',
|
||||
'.el-sub-menu__title:has-text("系统")',
|
||||
];
|
||||
|
||||
for (const sel of sysMenuSelectors) {
|
||||
const el = page.locator(sel).first();
|
||||
if (await el.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
const text = await el.textContent();
|
||||
await el.click();
|
||||
console.log(`[OK] 点击展开子菜单: "${text?.trim()}" (${sel})`);
|
||||
sysMenuFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sysMenuFound) {
|
||||
// 点击第一个子菜单
|
||||
const firstSubMenu = page.locator('.el-sub-menu__title').first();
|
||||
if (await firstSubMenu.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
const text = await firstSubMenu.textContent();
|
||||
await firstSubMenu.click();
|
||||
console.log(`[OK] 点击第一个子菜单: "${text?.trim()}"`);
|
||||
sysMenuFound = true;
|
||||
} else {
|
||||
console.log('[WARN] 未找到任何子菜单标题');
|
||||
}
|
||||
}
|
||||
|
||||
await page.waitForTimeout(800);
|
||||
await saveScreenshot(page, '03-submenu-expanded');
|
||||
|
||||
// 检查子菜单项是否展开
|
||||
const expandedItems = await page.evaluate(() => {
|
||||
const items = document.querySelectorAll('.el-sub-menu.is-opened .el-menu-item');
|
||||
return Array.from(items).map((el) => el.textContent?.trim()).filter(Boolean);
|
||||
});
|
||||
console.log('展开的子菜单项:', JSON.stringify(expandedItems));
|
||||
|
||||
// ==================== 步骤 4:点击用户管理 ====================
|
||||
console.log('\n=== 步骤 4:点击"用户管理" ===');
|
||||
const urlBefore4 = page.url();
|
||||
console.log('点击前 URL:', urlBefore4);
|
||||
|
||||
let userMenuClicked = false;
|
||||
const userMenuSelectors = [
|
||||
'.el-menu-item:has-text("用户管理")',
|
||||
'.el-menu-item:has-text("用户")',
|
||||
'li.el-menu-item:has-text("用户")',
|
||||
];
|
||||
|
||||
for (const sel of userMenuSelectors) {
|
||||
const el = page.locator(sel).first();
|
||||
if (await el.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await el.click();
|
||||
console.log(`[OK] 点击用户管理 (${sel})`);
|
||||
userMenuClicked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!userMenuClicked && expandedItems.length > 0) {
|
||||
// 点击第一个展开的子菜单项
|
||||
const firstItem = page.locator('.el-sub-menu.is-opened .el-menu-item').first();
|
||||
if (await firstItem.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
const text = await firstItem.textContent();
|
||||
await firstItem.click();
|
||||
console.log(`[OK] 点击第一个展开的子菜单项: "${text?.trim()}"`);
|
||||
userMenuClicked = true;
|
||||
}
|
||||
}
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
const urlAfter4 = page.url();
|
||||
const urlChanged4 = urlAfter4 !== urlBefore4;
|
||||
console.log('点击后 URL:', urlAfter4);
|
||||
console.log(`URL 变化: ${urlChanged4 ? 'YES' : 'NO'}`);
|
||||
await saveScreenshot(page, '04-user-management');
|
||||
|
||||
// ==================== 步骤 5:点击角色管理 ====================
|
||||
console.log('\n=== 步骤 5:点击"角色管理" ===');
|
||||
const urlBefore5 = page.url();
|
||||
|
||||
let roleMenuClicked = false;
|
||||
const roleMenuSelectors = [
|
||||
'.el-menu-item:has-text("角色管理")',
|
||||
'.el-menu-item:has-text("角色")',
|
||||
];
|
||||
|
||||
for (const sel of roleMenuSelectors) {
|
||||
const el = page.locator(sel).first();
|
||||
if (await el.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
await el.click();
|
||||
console.log(`[OK] 点击角色管理 (${sel})`);
|
||||
roleMenuClicked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!roleMenuClicked) {
|
||||
console.log('[WARN] 未找到角色管理菜单项');
|
||||
}
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
const urlAfter5 = page.url();
|
||||
console.log('角色管理 URL:', urlAfter5);
|
||||
console.log(`URL 变化: ${urlAfter5 !== urlBefore5 ? 'YES' : 'NO'}`);
|
||||
await saveScreenshot(page, '05-role-management');
|
||||
|
||||
// ==================== 步骤 6:点击首页 ====================
|
||||
console.log('\n=== 步骤 6:点击"首页" ===');
|
||||
let homeClicked = false;
|
||||
const homeSelectors = [
|
||||
'.el-menu-item:has-text("首页")',
|
||||
'.el-menu-item:has-text("Dashboard")',
|
||||
'.el-menu-item:has-text("仪表盘")',
|
||||
'.el-menu > .el-menu-item:first-child',
|
||||
];
|
||||
|
||||
for (const sel of homeSelectors) {
|
||||
const el = page.locator(sel).first();
|
||||
if (await el.isVisible({ timeout: 2000 }).catch(() => false)) {
|
||||
const text = await el.textContent();
|
||||
await el.click();
|
||||
console.log(`[OK] 点击首页 (${sel}): "${text?.trim()}"`);
|
||||
homeClicked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!homeClicked) {
|
||||
console.log('[WARN] 未找到首页菜单项');
|
||||
}
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
const urlAfterHome = page.url();
|
||||
console.log('首页 URL:', urlAfterHome);
|
||||
await saveScreenshot(page, '06-home-after-navigate');
|
||||
|
||||
// ==================== 步骤 7:检查菜单文字样式 ====================
|
||||
console.log('\n=== 步骤 7:检查菜单文字样式可见性 ===');
|
||||
const styleCheck = await page.evaluate(() => {
|
||||
const results: Array<{ selector: string; color: string; bg: string; visible: boolean }> = [];
|
||||
|
||||
const targets = [
|
||||
'.el-menu-item',
|
||||
'.el-sub-menu__title',
|
||||
'.el-menu--dark .el-menu-item',
|
||||
];
|
||||
|
||||
for (const sel of targets) {
|
||||
const el = document.querySelector(sel);
|
||||
if (el) {
|
||||
const style = window.getComputedStyle(el);
|
||||
const color = style.color;
|
||||
const bg = style.backgroundColor;
|
||||
// 检查文字是否可见(不是透明/黑色 on 黑色背景)
|
||||
const visible = color !== 'rgba(0, 0, 0, 0)' && color !== 'transparent';
|
||||
results.push({ selector: sel, color, bg, visible });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
styleCheck.forEach((item) => {
|
||||
const status = item.visible ? '[OK]' : '[WARN]';
|
||||
console.log(`${status} ${item.selector}: color=${item.color}, bg=${item.bg}`);
|
||||
});
|
||||
|
||||
// ==================== 步骤 8:JS 错误汇总 ====================
|
||||
console.log('\n=== 步骤 8:JavaScript 错误汇总 ===');
|
||||
|
||||
const handleMouseleaveErrors = jsErrors.filter((e) => e.includes('handleMouseleave'));
|
||||
const componentErrors = (jsErrors.concat(jsWarnings)).filter((e) => e.includes('Failed to resolve component'));
|
||||
const functionErrors = jsErrors.filter((e) => e.includes('is not a function'));
|
||||
|
||||
console.log(`总错误数: ${jsErrors.length}`);
|
||||
console.log(`handleMouseleave 错误: ${handleMouseleaveErrors.length}`);
|
||||
console.log(`Failed to resolve component 错误: ${componentErrors.length}`);
|
||||
console.log(`is not a function 错误: ${functionErrors.length}`);
|
||||
|
||||
if (jsErrors.length > 0) {
|
||||
console.log('\n错误列表:');
|
||||
jsErrors.forEach((e, i) => console.log(` ${i + 1}. ${e}`));
|
||||
}
|
||||
if (jsWarnings.length > 0) {
|
||||
console.log('\n警告列表:');
|
||||
jsWarnings.forEach((w, i) => console.log(` ${i + 1}. ${w}`));
|
||||
}
|
||||
|
||||
// ==================== 断言 ====================
|
||||
console.log('\n=== 断言验证 ===');
|
||||
|
||||
// 1. 关键错误不应存在
|
||||
expect(handleMouseleaveErrors.length, 'handleMouseleave is not a function 错误已修复').toBe(0);
|
||||
expect(functionErrors.length, 'is not a function 错误应为 0').toBe(0);
|
||||
|
||||
// 2. 菜单应该有内容
|
||||
expect(menuInfo.menuItemCount + menuInfo.subMenuCount, '侧边栏应有菜单项').toBeGreaterThan(0);
|
||||
|
||||
// 3. 至少有一次 URL 变化(点击菜单后导航有效)
|
||||
const anyNavigation = urlChanged4 || (urlAfter5 !== urlBefore5);
|
||||
expect(anyNavigation, '点击菜单后应发生页面导航').toBeTruthy();
|
||||
|
||||
// 4. 最终页面不应停留在登录页
|
||||
expect(urlAfterHome, '最终 URL 不应包含 /login').not.toContain('/login');
|
||||
|
||||
console.log('\n=== 测试完成 ===');
|
||||
console.log(`截图目录: ${SCREENSHOT_DIR}`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
jest: true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import Hamburger from '@/components/Hamburger/index.vue'
|
||||
describe('Hamburger.vue', () => {
|
||||
it('toggle click', async() => {
|
||||
const wrapper = shallowMount(Hamburger)
|
||||
await wrapper.find('.hamburger').trigger('click')
|
||||
expect(wrapper.emitted('toggleClick')).toBeTruthy()
|
||||
})
|
||||
it('prop isActive', async() => {
|
||||
const wrapper = shallowMount(Hamburger)
|
||||
await wrapper.setProps({ isActive: true })
|
||||
expect(wrapper.find('.is-active').exists()).toBe(true)
|
||||
await wrapper.setProps({ isActive: false })
|
||||
expect(wrapper.find('.is-active').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
describe('SvgIcon.vue', () => {
|
||||
it('iconClass', () => {
|
||||
const wrapper = shallowMount(SvgIcon, {
|
||||
props: {
|
||||
iconClass: 'test'
|
||||
}
|
||||
})
|
||||
expect(wrapper.find('use').attributes().href).toBe('#icon-test')
|
||||
})
|
||||
it('className', async() => {
|
||||
const wrapper = shallowMount(SvgIcon, {
|
||||
props: {
|
||||
iconClass: 'test'
|
||||
}
|
||||
})
|
||||
expect(wrapper.classes().length).toBe(1)
|
||||
await wrapper.setProps({ className: 'test' })
|
||||
expect(wrapper.classes().includes('test')).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import permisaction from '@/directive/permission/permisaction'
|
||||
|
||||
jest.mock('@/store', () => ({
|
||||
getters: {
|
||||
permisaction: ['admin:sysUser:add', 'admin:sysUser:edit']
|
||||
}
|
||||
}))
|
||||
|
||||
// 指令通过 el.parentNode.removeChild(el) 移除元素,因此被测元素必须有父节点
|
||||
const factory = value =>
|
||||
mount(
|
||||
{
|
||||
template: '<div><button v-permisaction="value">操作</button></div>',
|
||||
data: () => ({ value })
|
||||
},
|
||||
{
|
||||
global: { directives: { permisaction } }
|
||||
}
|
||||
)
|
||||
|
||||
describe('v-permisaction', () => {
|
||||
it('拥有权限时保留元素', () => {
|
||||
const wrapper = factory(['admin:sysUser:add'])
|
||||
expect(wrapper.find('button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('缺少权限时移除元素', () => {
|
||||
const wrapper = factory(['admin:sysUser:delete'])
|
||||
expect(wrapper.find('button').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('权限值为空数组时抛出错误', () => {
|
||||
expect(() => factory([])).toThrow('请设置操作权限标签值')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import permission from '@/directive/permission/permission'
|
||||
|
||||
jest.mock('@/store', () => ({
|
||||
getters: {
|
||||
roles: ['editor']
|
||||
}
|
||||
}))
|
||||
|
||||
// 指令通过 el.parentNode.removeChild(el) 移除元素,因此被测元素必须有父节点
|
||||
const factory = value =>
|
||||
mount(
|
||||
{
|
||||
template: '<div><section v-permission="value">内容</section></div>',
|
||||
data: () => ({ value })
|
||||
},
|
||||
{
|
||||
global: { directives: { permission } }
|
||||
}
|
||||
)
|
||||
|
||||
describe('v-permission', () => {
|
||||
it('角色匹配时保留元素', () => {
|
||||
const wrapper = factory(['editor'])
|
||||
expect(wrapper.find('section').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('角色不匹配时移除元素', () => {
|
||||
const wrapper = factory(['admin'])
|
||||
expect(wrapper.find('section').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('角色值为空数组时抛出错误', () => {
|
||||
expect(() => factory([])).toThrow('need roles!')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import { formatTime } from '@/utils/index.js'
|
||||
describe('Utils:formatTime', () => {
|
||||
const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
|
||||
const retrofit = 5 * 1000
|
||||
|
||||
it('ten digits timestamp', () => {
|
||||
expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分')
|
||||
})
|
||||
it('test now', () => {
|
||||
expect(formatTime(+new Date() - 1)).toBe('刚刚')
|
||||
})
|
||||
it('less two minute', () => {
|
||||
expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前')
|
||||
})
|
||||
it('less two hour', () => {
|
||||
expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前')
|
||||
})
|
||||
it('less one day', () => {
|
||||
expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前')
|
||||
})
|
||||
it('more than one day', () => {
|
||||
expect(formatTime(d)).toBe('7月13日17时54分')
|
||||
})
|
||||
it('format', () => {
|
||||
expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
|
||||
expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
|
||||
expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
import { parseTime } from '@/utils/index.js'
|
||||
describe('Utils:parseTime', () => {
|
||||
const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
|
||||
it('timestamp', () => {
|
||||
expect(parseTime(d)).toBe('2018-07-13 17:54:01')
|
||||
})
|
||||
it('ten digits timestamp', () => {
|
||||
expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01')
|
||||
})
|
||||
it('new Date', () => {
|
||||
expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01')
|
||||
})
|
||||
it('format', () => {
|
||||
expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
|
||||
expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
|
||||
expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
|
||||
})
|
||||
it('get the day of the week', () => {
|
||||
expect(parseTime(d, '{a}')).toBe('五') // 星期五
|
||||
})
|
||||
it('get the day of the week', () => {
|
||||
expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日
|
||||
})
|
||||
it('empty argument', () => {
|
||||
expect(parseTime()).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import { validUsername, validURL, validLowerCase, validUpperCase, validAlphabets } from '@/utils/validate.js'
|
||||
describe('Utils:validate', () => {
|
||||
it('validUsername', () => {
|
||||
expect(validUsername('admin')).toBe(true)
|
||||
expect(validUsername('editor')).toBe(true)
|
||||
expect(validUsername('xxxx')).toBe(false)
|
||||
})
|
||||
it('validURL', () => {
|
||||
expect(validURL('https://github.com/PanJiaChen/vue-element-admin')).toBe(true)
|
||||
expect(validURL('http://github.com/PanJiaChen/vue-element-admin')).toBe(true)
|
||||
expect(validURL('github.com/PanJiaChen/vue-element-admin')).toBe(false)
|
||||
})
|
||||
it('validLowerCase', () => {
|
||||
expect(validLowerCase('abc')).toBe(true)
|
||||
expect(validLowerCase('Abc')).toBe(false)
|
||||
expect(validLowerCase('123abc')).toBe(false)
|
||||
})
|
||||
it('validUpperCase', () => {
|
||||
expect(validUpperCase('ABC')).toBe(true)
|
||||
expect(validUpperCase('Abc')).toBe(false)
|
||||
expect(validUpperCase('123ABC')).toBe(false)
|
||||
})
|
||||
it('validAlphabets', () => {
|
||||
expect(validAlphabets('ABC')).toBe(true)
|
||||
expect(validAlphabets('Abc')).toBe(true)
|
||||
expect(validAlphabets('123aBC')).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user