fix: improve UI accessibility and responsive layouts
This commit is contained in:
1 parent
cf40abe7ee
commit
89bdbc7aea
56 files changed
+4564
-3815
No files matched your search
@@ -0,0 +1,76 @@
|
||||
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
|
||||
(async () => {
|
||||
const { createServer } = await import('vite');
|
||||
const react = (await import('@vitejs/plugin-react')).default;
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const server = await createServer({ configFile: false, root, plugins: [react()], logLevel: 'error', server: { host: '127.0.0.1', port: 1421, strictPort: false } });
|
||||
let browser;
|
||||
try {
|
||||
await server.listen();
|
||||
const address = server.httpServer.address();
|
||||
const base = `http://127.0.0.1:${typeof address === 'object' && address ? address.port : 1421}/`;
|
||||
const response = await fetch(base);
|
||||
assert.equal(response.ok, true, `Vite server failed at ${base}`);
|
||||
await response.text();
|
||||
const channel = process.env.PLAYWRIGHT_CHANNEL || (process.platform === 'win32' ? 'msedge' : undefined);
|
||||
browser = await chromium.launch(channel ? { channel, headless: true } : { headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
|
||||
await page.goto(`${base}?mock=running`, { waitUntil: 'commit' });
|
||||
await page.locator('.app-shell').waitFor();
|
||||
|
||||
assert.equal(await page.getByRole('heading', { level: 1, name: '首页' }).count(), 1);
|
||||
|
||||
await page.getByRole('button', { name: '使用记录' }).click();
|
||||
const tabs = page.getByRole('tab');
|
||||
assert.equal(await tabs.count(), 5);
|
||||
assert.deepEqual(await tabs.evaluateAll((elements) => elements
|
||||
.map((element) => element.getAttribute('aria-controls'))
|
||||
.filter((id) => !id || !document.getElementById(id))), []);
|
||||
await tabs.first().focus();
|
||||
await page.keyboard.press('ArrowRight');
|
||||
assert.equal(await tabs.nth(1).getAttribute('aria-selected'), 'true');
|
||||
await page.getByRole('tab', { name: /数据管理/ }).click();
|
||||
assert.equal(await page.locator('.usage-filter-panel').count(), 0);
|
||||
|
||||
await page.getByRole('button', { name: '高级功能' }).click();
|
||||
assert.equal(await page.locator('h1').count(), 1);
|
||||
const configTabs = page.locator('.config-subpage-tabs').getByRole('tab');
|
||||
assert.deepEqual(await configTabs.evaluateAll((elements) => elements
|
||||
.map((element) => element.getAttribute('aria-controls'))
|
||||
.filter((id) => !id || !document.getElementById(id))), []);
|
||||
const addKey = page.getByRole('button', { name: /新增鉴权密钥/ }).first();
|
||||
await addKey.focus();
|
||||
await addKey.click();
|
||||
const dialog = page.getByRole('dialog');
|
||||
await dialog.waitFor();
|
||||
for (let index = 0; index < 8; index += 1) {
|
||||
await page.keyboard.press('Tab');
|
||||
assert.equal(await page.evaluate(() => Boolean(document.activeElement?.closest('[role="dialog"]'))), true);
|
||||
}
|
||||
await page.keyboard.press('Escape');
|
||||
await dialog.waitFor({ state: 'detached' });
|
||||
await page.waitForFunction(() => document.activeElement?.getAttribute('aria-label') === '新增鉴权密钥');
|
||||
assert.equal(await addKey.evaluate((element) => document.activeElement === element), true);
|
||||
|
||||
await page.setViewportSize({ width: 640, height: 600 });
|
||||
await page.getByRole('button', { name: '首页' }).click();
|
||||
const sidebarHeight = await page.locator('.sidebar').evaluate((element) => element.getBoundingClientRect().height);
|
||||
assert.ok(sidebarHeight < 150, `compact sidebar is too tall: ${sidebarHeight}px`);
|
||||
|
||||
await page.locator('.sidebar-easy-entry').click();
|
||||
const steps = page.locator('.simple-mode-step-status-item');
|
||||
assert.equal(await steps.count(), 2);
|
||||
assert.equal(await steps.first().evaluate((element) => element.tagName), 'BUTTON');
|
||||
|
||||
console.log('PASS: page hierarchy, tabs, contextual filters, modal focus and compact shell.');
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
await server.close();
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -63,6 +63,14 @@ describe('browser mock runtime', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('uses normalized ratios for percentage-based usage metrics', async () => {
|
||||
const runtime = createBrowserMockRuntime('running');
|
||||
expect(await runtime.invoke('get_usage_overview')).toMatchObject({
|
||||
successRate: 96.47,
|
||||
cacheHitRate: 0.386,
|
||||
});
|
||||
});
|
||||
|
||||
test('provides an explicit error scenario', async () => {
|
||||
const runtime = createBrowserMockRuntime('error');
|
||||
await expect(runtime.invoke('get_core_status')).rejects.toThrow('Browser Mock error scenario');
|
||||
|
||||
Vendored
+1
-1
@@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client';
|
||||
import { mockIPC } from '@tauri-apps/api/mocks';
|
||||
import { AgentsPage } from '../../src/pages/AgentsPage';
|
||||
import { I18nProvider } from '../../src/i18n';
|
||||
import '../../src/styles.css';
|
||||
import '../../src/styles/index.css';
|
||||
const params = new URLSearchParams(location.search);
|
||||
if(params.has('reset-selections'))localStorage.removeItem('cpa-gui.agent-model-selections.v1');
|
||||
localStorage.setItem('easy-cli-proxy-api.locale',params.get('locale') || 'zh-CN');
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { I18nProvider } from '../../src/i18n';
|
||||
import { AuthFileManagementPage } from '../../src/pages/AuthFileManagementPage';
|
||||
import { QuotaPage } from '../../src/pages/QuotaPage';
|
||||
import { updateQuotaCache } from '../../src/services/quotaCache';
|
||||
import '../../src/styles.css';
|
||||
import '../../src/styles/index.css';
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
localStorage.setItem('easy-cli-proxy-api.locale', params.get('locale') || 'zh-CN');
|
||||
|
||||
Vendored
+1
-1
@@ -5,7 +5,7 @@ import { OAuthLoginPage } from '../../src/pages/ManagementPages';
|
||||
import { QuotaPage } from '../../src/pages/QuotaPage';
|
||||
import { AuthFileManagementPage } from '../../src/pages/AuthFileManagementPage';
|
||||
import { EasyModePage } from '../../src/pages/EasyModePage';
|
||||
import '../../src/styles.css';
|
||||
import '../../src/styles/index.css';
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const view = params.get('view');
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client';
|
||||
import { mockIPC } from '@tauri-apps/api/mocks';
|
||||
import { I18nProvider } from '../../src/i18n';
|
||||
import { ThinkingAliasesPage } from '../../src/pages/ThinkingAliasesPage';
|
||||
import '../../src/styles.css';
|
||||
import '../../src/styles/index.css';
|
||||
|
||||
localStorage.setItem('easy-cli-proxy-api.locale', 'en');
|
||||
const calls: { cmd: string; args: Record<string, unknown> }[] = [];
|
||||
|
||||
Vendored
+1
-1
@@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client';
|
||||
import { mockIPC } from '@tauri-apps/api/mocks';
|
||||
import { I18nProvider } from '../../src/i18n';
|
||||
import { ApiProviderDialog, type ProviderDraft } from '../../src/pages/ApiAccessPage';
|
||||
import '../../src/styles.css';
|
||||
import '../../src/styles/index.css';
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const scenario = params.get('scenario') ?? 'saved';
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { UsageRecordsPage } from '../../src/pages/UsageRecordsPage';
|
||||
import { AgentConfigurationFeedback } from '../../src/pages/AgentControls';
|
||||
import { useConfirmation } from '../../src/components/ConfirmationDialog';
|
||||
import { QuotaActionFeedback } from '../../src/components/QuotaActionFeedback';
|
||||
import '../../src/styles.css';
|
||||
import '../../src/styles/index.css';
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
localStorage.setItem('easy-cli-proxy-api.locale', params.get('locale') ?? 'zh-CN');
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client';
|
||||
import { mockIPC } from '@tauri-apps/api/mocks';
|
||||
import { I18nProvider } from '../../src/i18n';
|
||||
import { ApiAccessPage, providerRemarkIdentity, type ApiAccessRemarkLocator, type ProviderSection } from '../../src/pages/ApiAccessPage';
|
||||
import '../../src/styles.css';
|
||||
import '../../src/styles/index.css';
|
||||
|
||||
localStorage.setItem('easy-cli-proxy-api.locale', 'en');
|
||||
const fixture = window as typeof window & {
|
||||
|
||||
Vendored
+1
-1
@@ -6,7 +6,7 @@ import App from '../../src/App';
|
||||
import { I18nProvider } from '../../src/i18n';
|
||||
import { initializeTheme } from '../../src/theme';
|
||||
import type { AppTheme } from '../../src/themeController';
|
||||
import '../../src/styles.css';
|
||||
import '../../src/styles/index.css';
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const platform = params.get('platform') || 'windows';
|
||||
|
||||
Vendored
+1
-1
@@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client';
|
||||
import { mockIPC } from '@tauri-apps/api/mocks';
|
||||
import { I18nProvider } from '../../src/i18n';
|
||||
import { UsageRecordsPage } from '../../src/pages/UsageRecordsPage';
|
||||
import '../../src/styles.css';
|
||||
import '../../src/styles/index.css';
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
localStorage.setItem('easy-cli-proxy-api.locale', params.get('locale') || 'zh-TW');
|
||||
|
||||
@@ -2,7 +2,11 @@ import { describe, expect, it } from 'bun:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import postcss from 'postcss';
|
||||
|
||||
const stylesheet = postcss.parse(readFileSync(new URL('../src/styles.css', import.meta.url), 'utf8'));
|
||||
const stylesheet = postcss.parse([
|
||||
'../src/styles.css',
|
||||
'../src/styles/feature-pages.css',
|
||||
'../src/styles/ui-refinements.css',
|
||||
].map((file) => readFileSync(new URL(file, import.meta.url), 'utf8')).join('\n'));
|
||||
const aliases: Record<string, string> = {
|
||||
'--theme-4b4742': '--text-secondary',
|
||||
'--theme-59544d': '--text-secondary',
|
||||
|
||||
@@ -15,8 +15,16 @@ const technicalText = new Set([
|
||||
'headers',
|
||||
'ms',
|
||||
'note',
|
||||
'Models.dev',
|
||||
'LiteLLM',
|
||||
]);
|
||||
const technicalPlaceholders = new Set([
|
||||
'1h',
|
||||
'sk-...',
|
||||
'gpt-5.6-terra',
|
||||
'https://...',
|
||||
'socks5://127.0.0.1:1080',
|
||||
]);
|
||||
const technicalPlaceholders = new Set(['1h', 'sk-...', 'gpt-5.6-terra', 'https://...']);
|
||||
|
||||
function componentFiles(directory: string): string[] {
|
||||
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
|
||||
Reference in new issue
Block a user