合并远程主分支客户端收口
This commit is contained in:
@@ -1,25 +1,43 @@
|
||||
import { closeElectronApp, expect, test } from './fixtures/electron';
|
||||
|
||||
test.describe('Makelore Electron smoke flows', () => {
|
||||
test('loads the bundled mixed-script UI font', async ({ page }) => {
|
||||
await page.evaluate(async () => {
|
||||
await document.fonts.load('400 16px "Makelore Latin"', 'Makelore');
|
||||
await document.fonts.load('400 16px "Makelore CJK"', '中文');
|
||||
await document.fonts.load('600 16px "Makelore Latin"', 'Makelore');
|
||||
await document.fonts.load('600 16px "Makelore CJK"', '中文');
|
||||
});
|
||||
|
||||
await expect.poll(async () => page.evaluate(() => getComputedStyle(document.body).fontFamily))
|
||||
.toContain('Makelore Latin');
|
||||
await expect.poll(async () => page.evaluate(() => (
|
||||
Array.from(document.fonts).some((font) => font.family === 'Makelore CJK' && font.status === 'loaded')
|
||||
))).toBe(true);
|
||||
});
|
||||
|
||||
test('shows the setup wizard on a fresh profile', async ({ page }) => {
|
||||
await expect(page.getByTestId('setup-page')).toBeVisible();
|
||||
await expect(page.getByTestId('setup-welcome-step')).toBeVisible();
|
||||
await expect(page.getByTestId('setup-skip-button')).toBeVisible();
|
||||
});
|
||||
|
||||
test('can skip setup and continue to the login page', async ({ page }) => {
|
||||
test('can skip setup and open the public module chooser', async ({ page }) => {
|
||||
await expect(page.getByTestId('setup-page')).toBeVisible();
|
||||
await page.getByTestId('setup-skip-button').click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Continue in browser' })).toBeVisible();
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
await expect(page.getByTestId('main-layout')).toHaveCount(0);
|
||||
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await expect(page.getByRole('button', { name: '在浏览器中继续' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('persists skipped setup across relaunch for the same isolated profile', async ({ electronApp, launchElectronApp }) => {
|
||||
const firstWindow = await electronApp.firstWindow();
|
||||
await firstWindow.waitForLoadState('domcontentloaded');
|
||||
await firstWindow.getByTestId('setup-skip-button').click();
|
||||
await expect(firstWindow.getByRole('button', { name: 'Continue in browser' })).toBeVisible();
|
||||
await expect(firstWindow.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
|
||||
await closeElectronApp(electronApp);
|
||||
|
||||
@@ -28,7 +46,7 @@ test.describe('Makelore Electron smoke flows', () => {
|
||||
const relaunchedWindow = await relaunchedApp.firstWindow();
|
||||
await relaunchedWindow.waitForLoadState('domcontentloaded');
|
||||
|
||||
await expect(relaunchedWindow.getByRole('button', { name: 'Continue in browser' })).toBeVisible();
|
||||
await expect(relaunchedWindow.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
await expect(relaunchedWindow.getByTestId('setup-page')).toHaveCount(0);
|
||||
} finally {
|
||||
await closeElectronApp(relaunchedApp);
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { completeSetup, expect, test } from './fixtures/electron';
|
||||
|
||||
const testConfigResponses = {
|
||||
channelsAccounts: {
|
||||
success: true,
|
||||
channels: [
|
||||
{
|
||||
channelType: 'feishu',
|
||||
defaultAccountId: 'default',
|
||||
status: 'connected',
|
||||
accounts: [
|
||||
{
|
||||
accountId: 'default',
|
||||
name: 'Primary Account',
|
||||
configured: true,
|
||||
status: 'connected',
|
||||
isDefault: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
agents: {
|
||||
success: true,
|
||||
agents: [],
|
||||
},
|
||||
credentialsValidate: {
|
||||
success: true,
|
||||
valid: true,
|
||||
warnings: [],
|
||||
},
|
||||
channelConfig: {
|
||||
success: true,
|
||||
},
|
||||
};
|
||||
|
||||
test.describe('Channels account ID validation', () => {
|
||||
test('rejects non-canonical custom account ID before save', async ({ electronApp, page }) => {
|
||||
await electronApp.evaluate(({ ipcMain }, responses) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).__niancodeE2eChannelConfigSaveCount = 0;
|
||||
ipcMain.removeHandler('hostapi:fetch');
|
||||
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string; method?: string }) => {
|
||||
const method = request?.method ?? 'GET';
|
||||
const path = request?.path ?? '';
|
||||
|
||||
if (path === '/api/channels/accounts' && method === 'GET') {
|
||||
return { ok: true, data: { status: 200, ok: true, json: responses.channelsAccounts } };
|
||||
}
|
||||
if (path === '/api/agents' && method === 'GET') {
|
||||
return { ok: true, data: { status: 200, ok: true, json: responses.agents } };
|
||||
}
|
||||
if (path === '/api/channels/credentials/validate' && method === 'POST') {
|
||||
return { ok: true, data: { status: 200, ok: true, json: responses.credentialsValidate } };
|
||||
}
|
||||
if (path === '/api/channels/config' && method === 'POST') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).__niancodeE2eChannelConfigSaveCount += 1;
|
||||
return { ok: true, data: { status: 200, ok: true, json: responses.channelConfig } };
|
||||
}
|
||||
if (path.startsWith('/api/channels/config/') && method === 'GET') {
|
||||
return { ok: true, data: { status: 200, ok: true, json: { success: true, values: {} } } };
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
error: { message: `Unexpected hostapi:fetch request: ${method} ${path}` },
|
||||
};
|
||||
});
|
||||
}, testConfigResponses);
|
||||
|
||||
await completeSetup(page);
|
||||
|
||||
await page.getByTestId('sidebar-nav-channels').click();
|
||||
await expect(page.getByTestId('channels-page')).toBeVisible();
|
||||
await expect(page.getByText('Feishu / Lark')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: /Add Account|account\.add/i }).click();
|
||||
await expect(page.getByText(/Configure Feishu \/ Lark|dialog\.configureTitle/)).toBeVisible();
|
||||
|
||||
await page.locator('#account-id').fill('测试账号');
|
||||
await page.locator('#appId').fill('cli_test');
|
||||
await page.locator('#appSecret').fill('secret_test');
|
||||
|
||||
await page.getByRole('button', { name: /Save & Connect|dialog\.saveAndConnect/ }).click();
|
||||
await expect(page.getByText(/account\.invalidCanonicalId|must use lowercase letters/i).first()).toBeVisible();
|
||||
|
||||
const saveCalls = await electronApp.evaluate(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const count = Number((globalThis as any).__niancodeE2eChannelConfigSaveCount || 0);
|
||||
return { count };
|
||||
});
|
||||
expect(saveCalls.count).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,133 +0,0 @@
|
||||
import { completeSetup, expect, test } from './fixtures/electron';
|
||||
|
||||
test.describe('Channels binding regression', () => {
|
||||
test('keeps newly added non-default Feishu accounts unassigned until the user binds an agent', async ({ electronApp, page }) => {
|
||||
await electronApp.evaluate(({ ipcMain }) => {
|
||||
const state = {
|
||||
nextAccountId: 'feishu-a1b2c3d4',
|
||||
saveCount: 0,
|
||||
bindingCount: 0,
|
||||
channels: [
|
||||
{
|
||||
channelType: 'feishu',
|
||||
defaultAccountId: 'default',
|
||||
status: 'connected',
|
||||
accounts: [
|
||||
{
|
||||
accountId: 'default',
|
||||
name: 'Primary Account',
|
||||
configured: true,
|
||||
status: 'connected',
|
||||
isDefault: true,
|
||||
agentId: 'main',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
agents: [
|
||||
{ id: 'main', name: 'Main Agent' },
|
||||
{ id: 'code', name: 'Code Agent' },
|
||||
],
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).__niancodeE2eBindingRegression = state;
|
||||
|
||||
ipcMain.removeHandler('hostapi:fetch');
|
||||
ipcMain.handle('hostapi:fetch', async (_event, request: { path?: string; method?: string; body?: string }) => {
|
||||
const method = request?.method ?? 'GET';
|
||||
const path = request?.path ?? '';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const current = (globalThis as any).__niancodeE2eBindingRegression as typeof state;
|
||||
|
||||
if (path === '/api/channels/accounts' && method === 'GET') {
|
||||
return { ok: true, data: { status: 200, ok: true, json: { success: true, channels: current.channels } } };
|
||||
}
|
||||
if (path === '/api/agents' && method === 'GET') {
|
||||
return { ok: true, data: { status: 200, ok: true, json: { success: true, agents: current.agents } } };
|
||||
}
|
||||
if (path === '/api/channels/credentials/validate' && method === 'POST') {
|
||||
return { ok: true, data: { status: 200, ok: true, json: { success: true, valid: true, warnings: [] } } };
|
||||
}
|
||||
if (path === '/api/channels/config' && method === 'POST') {
|
||||
current.saveCount += 1;
|
||||
const body = JSON.parse(request?.body ?? '{}') as { accountId?: string };
|
||||
const accountId = body.accountId || current.nextAccountId;
|
||||
const feishu = current.channels[0];
|
||||
if (!feishu.accounts.some((account) => account.accountId === accountId)) {
|
||||
feishu.accounts.push({
|
||||
accountId,
|
||||
name: accountId,
|
||||
configured: true,
|
||||
status: 'connected',
|
||||
isDefault: false,
|
||||
});
|
||||
}
|
||||
return { ok: true, data: { status: 200, ok: true, json: { success: true } } };
|
||||
}
|
||||
if (path === '/api/channels/binding' && method === 'PUT') {
|
||||
current.bindingCount += 1;
|
||||
const body = JSON.parse(request?.body ?? '{}') as { channelType?: string; accountId?: string; agentId?: string };
|
||||
if (body.channelType === 'feishu' && body.accountId) {
|
||||
const feishu = current.channels[0];
|
||||
const account = feishu.accounts.find((entry) => entry.accountId === body.accountId);
|
||||
if (account) {
|
||||
account.agentId = body.agentId;
|
||||
}
|
||||
}
|
||||
return { ok: true, data: { status: 200, ok: true, json: { success: true } } };
|
||||
}
|
||||
if (path === '/api/channels/binding' && method === 'DELETE') {
|
||||
current.bindingCount += 1;
|
||||
return { ok: true, data: { status: 200, ok: true, json: { success: true } } };
|
||||
}
|
||||
if (path.startsWith('/api/channels/config/') && method === 'GET') {
|
||||
return { ok: true, data: { status: 200, ok: true, json: { success: true, values: {} } } };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
error: { message: `Unexpected hostapi:fetch request: ${method} ${path}` },
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
await completeSetup(page);
|
||||
|
||||
await page.getByTestId('sidebar-nav-channels').click();
|
||||
await expect(page.getByTestId('channels-page')).toBeVisible();
|
||||
await expect(page.getByText('Feishu / Lark')).toBeVisible();
|
||||
|
||||
const feishuGroupHeader = page.locator('div.rounded-2xl').filter({ hasText: 'Feishu / Lark' }).first();
|
||||
await expect(feishuGroupHeader).toContainText(/Connected|已连接|接続済み|Подключён/);
|
||||
|
||||
await page.getByRole('button', { name: /Add Account|添加账号|アカウントを追加/ }).click();
|
||||
await expect(page.getByText(/Configure Feishu \/ Lark|dialog\.configureTitle/)).toBeVisible();
|
||||
|
||||
const accountIdInput = page.locator('#account-id');
|
||||
const newAccountId = await accountIdInput.inputValue();
|
||||
await expect(accountIdInput).toHaveValue(/feishu-/);
|
||||
await page.locator('#appId').fill('cli_test');
|
||||
await page.locator('#appSecret').fill('secret_test');
|
||||
|
||||
await page.getByRole('button', { name: /Save & Connect|dialog\.saveAndConnect/ }).click();
|
||||
await expect(page.getByText(/Configure Feishu \/ Lark|dialog\.configureTitle/)).toBeHidden();
|
||||
|
||||
const newAccountRow = page.locator('div.rounded-xl').filter({ hasText: newAccountId }).first();
|
||||
await expect(newAccountRow).toBeVisible();
|
||||
const bindingSelect = newAccountRow.locator('select');
|
||||
await expect(bindingSelect).toHaveValue('');
|
||||
|
||||
await bindingSelect.selectOption('code');
|
||||
await expect(bindingSelect).toHaveValue('code');
|
||||
|
||||
const counters = await electronApp.evaluate(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const state = (globalThis as any).__niancodeE2eBindingRegression as { saveCount: number; bindingCount: number };
|
||||
return { saveCount: state.saveCount, bindingCount: state.bindingCount };
|
||||
});
|
||||
|
||||
expect(counters.saveCount).toBe(1);
|
||||
expect(counters.bindingCount).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -277,7 +277,31 @@ export async function completeSetup(page: Page): Promise<void> {
|
||||
if (await setupPage.isVisible()) {
|
||||
await page.getByTestId('setup-skip-button').click();
|
||||
}
|
||||
const moduleSelectionPage = page.getByTestId('ai-module-selection-page');
|
||||
await expect(moduleSelectionPage).toBeVisible();
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
}
|
||||
|
||||
export async function openModelsPage(page: Page): Promise<void> {
|
||||
const settingsPage = page.getByTestId('settings-page');
|
||||
for (let attempt = 0; attempt < 2 && !(await settingsPage.isVisible()); attempt += 1) {
|
||||
const settingsLink = page.getByTestId('sidebar-nav-settings');
|
||||
if (!(await settingsLink.isVisible())) {
|
||||
await page.getByTestId('sidebar-member-menu-trigger').click();
|
||||
}
|
||||
await expect(settingsLink).toBeVisible();
|
||||
await settingsLink.click();
|
||||
await settingsPage.waitFor({ state: 'visible', timeout: 5_000 }).catch(() => undefined);
|
||||
}
|
||||
await expect(settingsPage).toBeVisible();
|
||||
await page.getByTestId('settings-reveal-additional').click();
|
||||
await page
|
||||
.getByTestId('settings-opencode-models')
|
||||
.getByRole('button', { name: 'Configure models' })
|
||||
.click();
|
||||
await expect(page.getByTestId('models-page')).toBeVisible();
|
||||
await expect(page.getByTestId('providers-settings')).toBeVisible();
|
||||
}
|
||||
|
||||
export { expect };
|
||||
|
||||
@@ -9,8 +9,8 @@ test.describe('AI Design conversations', () => {
|
||||
|
||||
try {
|
||||
const page = await getStableWindow(app);
|
||||
await page.getByTestId('sidebar-module-switcher-trigger').click();
|
||||
await page.getByTestId('sidebar-module-painting').click();
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
await page.getByTestId('ai-module-option-painting').click();
|
||||
|
||||
await page.getByTestId('sidebar-create-image-project').click();
|
||||
await page.getByLabel('项目名称').fill('多会话验收项目');
|
||||
|
||||
@@ -66,13 +66,15 @@ test.describe('Russian language localization', () => {
|
||||
|
||||
// Skip setup
|
||||
await page.getByTestId('setup-skip-button').click();
|
||||
await expect(page.getByRole('button', { name: 'Continue in browser' })).toBeVisible();
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
|
||||
await closeElectronApp(app);
|
||||
firstAppClosed = true;
|
||||
|
||||
relaunchedApp = await launchElectronApp({ skipSetup: true });
|
||||
const relaunchedPage = await getStableWindow(relaunchedApp);
|
||||
await expect(relaunchedPage.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
await relaunchedPage.getByTestId('ai-module-option-programming').click();
|
||||
await expect(relaunchedPage.getByTestId('main-layout')).toBeVisible();
|
||||
|
||||
// Bypass authentication only for the relaunch so the persisted setting
|
||||
@@ -96,6 +98,8 @@ test.describe('Russian language localization', () => {
|
||||
try {
|
||||
const page = await getStableWindow(app);
|
||||
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
|
||||
// Navigate to Settings (in English by default after skipSetup)
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
|
||||
test.describe('NianCode main navigation without setup flow', () => {
|
||||
test('navigates between core pages with setup bypassed', async ({ launchElectronApp }) => {
|
||||
test.describe('Makelore module navigation without setup flow', () => {
|
||||
test('opens the module chooser and switches between enabled modules', async ({ launchElectronApp }) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
|
||||
try {
|
||||
const page = await getStableWindow(app);
|
||||
const minimumSize = await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.getMinimumSize());
|
||||
|
||||
expect(minimumSize).toEqual([1100, 700]);
|
||||
expect(minimumSize).toEqual([1024, 700]);
|
||||
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
await expect(page.getByText('请先新建项目')).toHaveCount(0);
|
||||
await expect(page.getByText('一念成光,万物可创。')).toBeVisible();
|
||||
@@ -20,28 +22,11 @@ test.describe('NianCode main navigation without setup flow', () => {
|
||||
await expect(page.getByTestId('sidebar-nav-project-config')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-nav-agent-chat')).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '添加已有项目' })).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-nav-character')).toHaveText('角色场景');
|
||||
await expect(page.getByText('成长成就在这里')).toHaveCount(0);
|
||||
await expect(page.getByText('已创建 Phaser 2D 小游戏起步工程')).toHaveCount(0);
|
||||
await expect(page.getByTestId('game-asset-browser')).toHaveCount(0);
|
||||
await expect(page.getByTestId('game-asset-review-summary')).toHaveCount(0);
|
||||
|
||||
await page.getByTestId('resource-card-models').click();
|
||||
await expect(page.getByTestId('makelore-model-config-refresh-button')).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await page.getByTestId('resource-card-skills').click();
|
||||
await expect(page.getByRole('heading', { name: '可用技能库' })).toBeVisible();
|
||||
await expect(page.getByTestId('skill-library-card-youth-plain-language')).toContainText('青少年通俗表达');
|
||||
await expect(page.getByTestId('skill-library-card-youth-plain-language')).not.toContainText('youth-plain-language');
|
||||
await expect(page.getByTestId('skill-library-card-pm-project-plan')).toContainText('通用软件项目规划');
|
||||
await expect(page.getByTestId('skill-library-card-pm-project-plan')).not.toContainText('pm-project-plan');
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await page.getByTestId('sidebar-nav-models').click();
|
||||
await expect(page.getByTestId('models-page')).toBeVisible();
|
||||
await expect(page.getByTestId('models-page-title')).toBeVisible();
|
||||
|
||||
await expect(page.getByTestId('sidebar-module-switcher')).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-module-switcher-trigger')).toHaveAttribute('aria-expanded', 'false');
|
||||
await page.getByTestId('sidebar-module-switcher-trigger').click();
|
||||
@@ -50,28 +35,19 @@ test.describe('NianCode main navigation without setup flow', () => {
|
||||
await page.getByTestId('sidebar-module-painting').click();
|
||||
await expect(page.getByTestId('image-canvas-page')).toBeVisible();
|
||||
await expect(page.getByTestId('image-workspace-unavailable')).toBeVisible();
|
||||
await expect(page.getByText('创作空间暂不可用').first()).toBeVisible();
|
||||
await expect(page.getByText('AI 设计暂不可用').first()).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-image-workspace')).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-create-image-project')).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-image-projects')).toBeVisible();
|
||||
await expect(page.getByText('本地开发')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-reset-local-image-workspace')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-create-project')).toHaveCount(0);
|
||||
await expect(page.getByText('制作中心')).toHaveCount(0);
|
||||
await expect(page.getByText('任务模块')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-module-painting')).toHaveAttribute('aria-current', 'page');
|
||||
await page.getByTestId('sidebar-module-switcher-trigger').click();
|
||||
await page.getByTestId('sidebar-module-programming').click();
|
||||
await expect(page.getByText('请先新建项目')).toHaveCount(0);
|
||||
await expect(page.getByText('一念成光,万物可创。')).toBeVisible();
|
||||
await expect(page.getByTestId('chat-operation-page')).toHaveCount(0);
|
||||
await expect(page.getByTestId('sidebar-nav-asset-square')).toHaveCount(0);
|
||||
|
||||
await page.getByTestId('sidebar-nav-agents').click();
|
||||
await expect(page.getByTestId('agents-page')).toBeVisible();
|
||||
|
||||
await page.getByTestId('sidebar-nav-channels').click();
|
||||
await expect(page.getByTestId('channels-page')).toBeVisible();
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ async function installFirstChatHost(electronApp: ElectronApplication): Promise<v
|
||||
schemaVersion: 1,
|
||||
projectType: 'custom',
|
||||
initialized: true,
|
||||
superpowersEnabled: false,
|
||||
defaultModel: 'niancode-user-models/qwen3.7-plus',
|
||||
agents: [agent],
|
||||
knowledgeDirectory: 'knowledge',
|
||||
@@ -223,6 +222,8 @@ test('submits the first prompt without waiting for the known-empty session histo
|
||||
let page = await getStableWindow(electronApp);
|
||||
await page.reload();
|
||||
page = await getStableWindow(electronApp);
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
const agent = page.getByTestId('project-agent-chat-game-development');
|
||||
await agent.click();
|
||||
await expect(agent).toHaveAttribute('aria-pressed', 'true');
|
||||
|
||||
@@ -22,6 +22,36 @@ test.describe('OpenCode image compression composer', () => {
|
||||
updatedAt: '2026-07-18T00:00:00.000Z',
|
||||
lastOpenedAt: '2026-07-18T00:00:00.000Z',
|
||||
};
|
||||
const agent = {
|
||||
id: 'game-development',
|
||||
avatarId: 'avatar-01',
|
||||
roleName: 'Game Developer',
|
||||
name: 'Game Developer',
|
||||
builtIn: false,
|
||||
enabled: true,
|
||||
model: 'niancode-user-models/qwen3.7-plus',
|
||||
skillIds: [],
|
||||
responsibility: {
|
||||
mission: 'Build and verify the project.',
|
||||
owns: [],
|
||||
boundaries: [],
|
||||
collaborators: [],
|
||||
principles: [],
|
||||
},
|
||||
prompt: '',
|
||||
archivedAt: null,
|
||||
pinned: false,
|
||||
};
|
||||
const projectConfig = {
|
||||
schemaVersion: 1,
|
||||
projectType: 'custom',
|
||||
initialized: true,
|
||||
defaultModel: agent.model,
|
||||
agents: [agent],
|
||||
knowledgeDirectory: 'knowledge',
|
||||
createdAt: '2026-07-18T00:00:00.000Z',
|
||||
updatedAt: '2026-07-18T00:00:00.000Z',
|
||||
};
|
||||
const respond = (json: unknown, status = 200) => ({
|
||||
ok: true,
|
||||
data: {
|
||||
@@ -44,7 +74,37 @@ test.describe('OpenCode image compression composer', () => {
|
||||
url: 'http://127.0.0.1:4096',
|
||||
});
|
||||
}
|
||||
if (path.startsWith('/api/opencode/projects')) {
|
||||
if (path.startsWith('/api/opencode/projects/config?')) {
|
||||
return respond({
|
||||
status: 'valid',
|
||||
config: projectConfig,
|
||||
knowledgeFiles: [],
|
||||
});
|
||||
}
|
||||
if (path.startsWith('/api/opencode/projects/template?')) {
|
||||
return respond({ status: 'missing' });
|
||||
}
|
||||
if (path.startsWith('/api/opencode/projects/conversations?')) {
|
||||
return respond({
|
||||
state: {
|
||||
schemaVersion: 1,
|
||||
sessions: [{
|
||||
sessionId: 'ses_e2e_compression',
|
||||
agentId: agent.id,
|
||||
archivedAt: null,
|
||||
unreadCount: 0,
|
||||
createdAt: '2026-07-18T00:00:00.000Z',
|
||||
updatedAt: '2026-07-18T00:00:00.000Z',
|
||||
}],
|
||||
updatedAt: '2026-07-18T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
}
|
||||
if (
|
||||
path === '/api/opencode/projects'
|
||||
|| path.startsWith('/api/opencode/projects?')
|
||||
|| (path === '/api/opencode/projects/active' && method === 'GET')
|
||||
) {
|
||||
return respond({ projects: [activeProject], activeProject });
|
||||
}
|
||||
if (path === '/api/opencode/config-summary') {
|
||||
@@ -60,6 +120,7 @@ test.describe('OpenCode image compression composer', () => {
|
||||
sessions: [{
|
||||
id: 'ses_e2e_compression',
|
||||
title: 'Image compression',
|
||||
agent: agent.id,
|
||||
updatedAt: '2026-07-18T00:00:00.000Z',
|
||||
}],
|
||||
});
|
||||
@@ -88,9 +149,14 @@ test.describe('OpenCode image compression composer', () => {
|
||||
|
||||
const page = await getStableWindow(app);
|
||||
await page.reload();
|
||||
await expect(page.getByTestId('sidebar-nav-agent-chat')).toBeVisible();
|
||||
await page.getByTestId('sidebar-nav-agent-chat').click();
|
||||
await expect(page.getByTestId('opencode-message-composer')).toBeVisible();
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
const composer = page.getByTestId('opencode-message-composer');
|
||||
await expect(composer).toBeVisible();
|
||||
const projectAgent = page.getByTestId('project-agent-chat-game-development');
|
||||
await projectAgent.click();
|
||||
await expect(projectAgent).toHaveAttribute('aria-pressed', 'true');
|
||||
await expect(composer.getByRole('button').first()).toBeEnabled();
|
||||
|
||||
const png = await sharp({
|
||||
create: {
|
||||
|
||||
@@ -91,6 +91,36 @@ async function installSlashCommandHost(
|
||||
title: 'Slash E2E',
|
||||
agent: 'game-development',
|
||||
};
|
||||
const agent = {
|
||||
id: 'game-development',
|
||||
avatarId: 'avatar-01',
|
||||
roleName: 'Game Development',
|
||||
name: 'Game Development',
|
||||
builtIn: false,
|
||||
enabled: true,
|
||||
model: 'niancode-user-models/qwen3.7-plus',
|
||||
skillIds: [],
|
||||
responsibility: {
|
||||
mission: 'Implement and verify project features.',
|
||||
owns: [],
|
||||
boundaries: [],
|
||||
collaborators: [],
|
||||
principles: [],
|
||||
},
|
||||
prompt: '',
|
||||
archivedAt: null,
|
||||
pinned: false,
|
||||
};
|
||||
const config = {
|
||||
schemaVersion: 1,
|
||||
projectType: 'custom',
|
||||
initialized: true,
|
||||
defaultModel: 'niancode-user-models/qwen3.7-plus',
|
||||
agents: [agent],
|
||||
knowledgeDirectory: 'knowledge',
|
||||
createdAt: '2026-07-18T00:00:00.000Z',
|
||||
updatedAt: '2026-07-18T00:00:00.000Z',
|
||||
};
|
||||
const status = {
|
||||
state: 'running',
|
||||
port: 4096,
|
||||
@@ -149,13 +179,30 @@ async function installSlashCommandHost(
|
||||
}
|
||||
if (path.startsWith('/api/opencode/projects/config?')) {
|
||||
return respond({
|
||||
status: 'missing',
|
||||
status: 'valid',
|
||||
config,
|
||||
knowledgeFiles: [],
|
||||
});
|
||||
}
|
||||
if (path.startsWith('/api/opencode/projects/template?')) {
|
||||
return respond({ status: 'missing' });
|
||||
}
|
||||
if (path.startsWith('/api/opencode/projects/conversations?')) {
|
||||
return respond({
|
||||
state: {
|
||||
schemaVersion: 1,
|
||||
sessions: [{
|
||||
sessionId: session.id,
|
||||
agentId: agent.id,
|
||||
archivedAt: null,
|
||||
unreadCount: 0,
|
||||
createdAt: '2026-07-18T00:00:00.000Z',
|
||||
updatedAt: '2026-07-18T00:00:00.000Z',
|
||||
}],
|
||||
updatedAt: '2026-07-18T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
}
|
||||
if (path === '/api/opencode/config-summary') {
|
||||
return respond({
|
||||
model: 'niancode-user-models/qwen3.7-plus',
|
||||
@@ -166,16 +213,30 @@ async function installSlashCommandHost(
|
||||
});
|
||||
}
|
||||
if (path === '/api/provider-accounts') {
|
||||
return respond([]);
|
||||
return respond([{
|
||||
id: 'niancode-user-models',
|
||||
vendorId: 'custom',
|
||||
label: 'Makelore Models',
|
||||
authMode: 'api_key',
|
||||
model: 'qwen3.7-plus',
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
createdAt: '2026-07-18T00:00:00.000Z',
|
||||
updatedAt: '2026-07-18T00:00:00.000Z',
|
||||
}]);
|
||||
}
|
||||
if (path === '/api/provider-accounts/key-info') {
|
||||
return respond([]);
|
||||
return respond([{
|
||||
accountId: 'niancode-user-models',
|
||||
hasKey: true,
|
||||
keyMasked: 'sk-***',
|
||||
}]);
|
||||
}
|
||||
if (path === '/api/provider-vendors') {
|
||||
return respond([]);
|
||||
}
|
||||
if (path === '/api/provider-accounts/default') {
|
||||
return respond({ accountId: null });
|
||||
return respond({ accountId: 'niancode-user-models' });
|
||||
}
|
||||
if (path === '/api/opencode/sessions') {
|
||||
return respond({ sessions: [session] });
|
||||
@@ -270,18 +331,25 @@ test.describe('OpenCode slash commands', () => {
|
||||
await completeSetup(page);
|
||||
await installSlashCommandHost(electronApp);
|
||||
await page.reload();
|
||||
await page.getByTestId('sidebar-module-switcher-trigger').click();
|
||||
await page.getByTestId('sidebar-module-programming').click();
|
||||
await expect(page).toHaveURL(/\/opencode-chat$/);
|
||||
await page.getByTestId('project-agent-chat-game-development').click();
|
||||
const composer = page.getByRole('textbox');
|
||||
await expect(composer).toBeVisible();
|
||||
|
||||
await composer.fill('/comp');
|
||||
await expect(page.getByRole('option', { name: /compact/ })).toBeVisible();
|
||||
await composer.press('Enter');
|
||||
await expect(composer).toHaveValue('/compact');
|
||||
await expect(
|
||||
page.getByText('再次按 Enter 执行'),
|
||||
).toBeVisible();
|
||||
expect(await readCapturedRequests(electronApp)).toEqual([]);
|
||||
expect((await readCapturedRequests(electronApp)).some((request) => (
|
||||
request.path.endsWith('/summarize')
|
||||
|| request.path.endsWith('/command')
|
||||
|| request.path.endsWith('/messages')
|
||||
))).toBe(false);
|
||||
|
||||
await composer.press('Enter');
|
||||
await expect(composer).toHaveValue('');
|
||||
@@ -310,16 +378,20 @@ test.describe('OpenCode slash commands', () => {
|
||||
);
|
||||
|
||||
await setSlashCommandFailure(electronApp, 'review failed');
|
||||
const commandCountBeforeFailure = (await readCapturedRequests(electronApp))
|
||||
.filter((request) => request.path.endsWith('/command')).length;
|
||||
await composer.fill('/Rev');
|
||||
await expect(page.getByRole('option', { name: /Review/i })).toBeVisible();
|
||||
await composer.fill('/Review staged changes ');
|
||||
await composer.press('Enter');
|
||||
await expect(page.getByText('review failed')).toBeVisible();
|
||||
await expect.poll(async () => (
|
||||
await readCapturedRequests(electronApp)
|
||||
).filter((request) => request.path.endsWith('/command')).length)
|
||||
.toBe(commandCountBeforeFailure + 1);
|
||||
await expect(composer).toHaveValue(
|
||||
'/Review staged changes ',
|
||||
);
|
||||
|
||||
await setSlashCommandFailure(electronApp, null);
|
||||
await composer.press('Enter');
|
||||
await expect(composer).toHaveValue('');
|
||||
const command = (await readCapturedRequests(electronApp))
|
||||
.findLast((request) => request.path.endsWith('/command'));
|
||||
expect(command?.body).toMatchObject({
|
||||
@@ -329,4 +401,94 @@ test.describe('OpenCode slash commands', () => {
|
||||
model: 'niancode-user-models/qwen3.7-plus',
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps the project and conversation rails at the minimum width without covering the chat canvas', async ({
|
||||
electronApp,
|
||||
page,
|
||||
}) => {
|
||||
await completeSetup(page);
|
||||
await installSlashCommandHost(electronApp);
|
||||
await page.reload();
|
||||
await page.setViewportSize({ width: 1280, height: 800 });
|
||||
await page.getByTestId('sidebar-module-switcher-trigger').click();
|
||||
await page.getByTestId('sidebar-module-programming').click();
|
||||
await expect(page).toHaveURL(/\/opencode-chat$/);
|
||||
await expect(page.getByTestId('opencode-message-composer')).toBeVisible();
|
||||
|
||||
const createPartnerButton = page.getByRole('button', { name: '创建伙伴' });
|
||||
await expect(createPartnerButton).toHaveClass(/text-brand/);
|
||||
await expect(createPartnerButton).toHaveAttribute('aria-expanded', 'false');
|
||||
await createPartnerButton.click();
|
||||
const createPartnerDialog = page.getByRole('dialog', { name: '创建项目伙伴' });
|
||||
await expect(createPartnerDialog).toBeVisible();
|
||||
await expect(createPartnerDialog).toHaveCSS('z-index', '110');
|
||||
await createPartnerDialog.getByLabel(/伙伴名称/).fill('E2E层级检查');
|
||||
await page.getByRole('button', { name: '取消' }).click();
|
||||
await expect(createPartnerButton).toHaveClass(/text-brand/);
|
||||
|
||||
await expect(page.getByRole('heading', { name: '我的项目空间' })).toHaveCount(0);
|
||||
await page.getByTestId('agent-browser-panel-open').click();
|
||||
await expect(page.getByTestId('agent-browser-panel')).toBeVisible();
|
||||
const diagnostics = page.getByTestId('agent-browser-diagnostics');
|
||||
await expect(diagnostics).toHaveAttribute('data-state', 'closed');
|
||||
await page.getByRole('tab', { name: /Console/ }).click();
|
||||
await expect(diagnostics).toHaveAttribute('data-state', 'open');
|
||||
await expect(diagnostics).toHaveCSS('height', '260px');
|
||||
await page.getByRole('button', { name: '收起调试面板' }).click();
|
||||
await expect(diagnostics).toHaveAttribute('data-state', 'closed');
|
||||
|
||||
const rect = async (testId: string) => page.getByTestId(testId).evaluate((element) => {
|
||||
const box = element.getBoundingClientRect();
|
||||
return { top: box.top, left: box.left, right: box.right, width: box.width };
|
||||
});
|
||||
const [layout, project, conversations, conversationHeader, conversationContext, chat, browser] = await Promise.all([
|
||||
rect('opencode-chat-layout'),
|
||||
rect('sidebar'),
|
||||
rect('agent-conversation-sidebar'),
|
||||
rect('agent-conversation-sidebar-header'),
|
||||
rect('agent-conversation-dialog-context'),
|
||||
rect('opencode-chat-canvas'),
|
||||
rect('agent-browser-panel'),
|
||||
]);
|
||||
const isHeaderInsideConversationSidebar = async () => page.getByTestId('agent-conversation-sidebar').evaluate((sidebar) => sidebar.contains(document.querySelector('[data-testid="agent-conversation-sidebar-header"]')));
|
||||
|
||||
expect(project.width).toBeGreaterThanOrEqual(255);
|
||||
expect(project.width).toBeLessThan(257);
|
||||
expect(conversations.width).toBeGreaterThanOrEqual(255);
|
||||
expect(conversations.width).toBeLessThan(257);
|
||||
expect(Math.abs(conversations.top - project.top)).toBeLessThan(2);
|
||||
expect(Math.abs(conversationHeader.top)).toBeLessThan(2);
|
||||
expect(Math.abs(conversationHeader.left - conversations.left)).toBeLessThan(2);
|
||||
expect(Math.abs(conversationHeader.width - conversations.width)).toBeLessThan(2);
|
||||
expect(Math.abs(conversationContext.top)).toBeLessThan(2);
|
||||
expect(Math.abs(conversationContext.left - chat.left)).toBeLessThan(2);
|
||||
expect(await isHeaderInsideConversationSidebar()).toBe(false);
|
||||
expect(Math.abs(conversations.left - project.right)).toBeLessThan(2);
|
||||
expect(Math.abs(chat.left - conversations.right)).toBeLessThan(2);
|
||||
expect(Math.abs(browser.right - layout.right)).toBeLessThan(2);
|
||||
expect(browser.left).toBeGreaterThanOrEqual(chat.right - 1);
|
||||
|
||||
const readRailAlignment = async () => {
|
||||
const [collapsedProject, collapsedConversations] = await Promise.all([
|
||||
rect('sidebar'),
|
||||
rect('agent-conversation-sidebar'),
|
||||
]);
|
||||
return {
|
||||
projectWidth: collapsedProject.width,
|
||||
conversationLeft: collapsedConversations.left,
|
||||
projectRight: collapsedProject.right,
|
||||
alignmentDelta: Math.abs(collapsedConversations.left - collapsedProject.right),
|
||||
};
|
||||
};
|
||||
|
||||
await page.getByRole('button', { name: '折叠侧栏' }).click();
|
||||
await expect.poll(async () => (await readRailAlignment()).projectWidth).toBeLessThan(1);
|
||||
await expect.poll(async () => (await readRailAlignment()).conversationLeft).toBeLessThan(2);
|
||||
await expect.poll(isHeaderInsideConversationSidebar).toBe(false);
|
||||
|
||||
await page.getByRole('button', { name: '展开侧栏' }).click();
|
||||
await expect.poll(async () => (await readRailAlignment()).alignmentDelta).toBeLessThan(2);
|
||||
await expect.poll(async () => (await readRailAlignment()).projectWidth).toBeGreaterThan(255);
|
||||
await expect.poll(isHeaderInsideConversationSidebar).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
115
tests/e2e/project-configuration-skills.spec.ts
Normal file
115
tests/e2e/project-configuration-skills.spec.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
import { createProjectConfig } from '../../shared/project-config';
|
||||
|
||||
test.describe('Project configuration skills', () => {
|
||||
test('does not expose the removed Superpowers feature and supports Skill details', async ({ launchElectronApp }) => {
|
||||
const parentPath = await mkdtemp(path.join(tmpdir(), 'niancode-project-configuration-e2e-'));
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
|
||||
try {
|
||||
const userDataDir = await app.evaluate(({ app: electronApp }) => electronApp.getPath('userData'));
|
||||
const skillDir = path.join(userDataDir, 'opencode', 'niancode-config', 'skills', 'pm-project-plan');
|
||||
await mkdir(path.join(skillDir, 'references'), { recursive: true });
|
||||
await writeFile(path.join(skillDir, 'SKILL.md'), '---\nname: pm-project-plan\ndescription: E2E project planning skill.\n---\n\n# Project planning\n');
|
||||
await writeFile(path.join(skillDir, 'references', 'guide.md'), '# Reference guide\n');
|
||||
|
||||
await app.evaluate(({ ipcMain }, selectedPath) => {
|
||||
ipcMain.removeHandler('dialog:open');
|
||||
ipcMain.handle('dialog:open', async () => ({ canceled: false, filePaths: [selectedPath] }));
|
||||
}, parentPath);
|
||||
|
||||
const page = await getStableWindow(app);
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
await page.getByTestId('sidebar-create-project').click();
|
||||
await expect(page.getByRole('radio', { name: '小游戏' })).toBeChecked();
|
||||
await expect(page.getByRole('radio', { name: '小程序' })).not.toBeChecked();
|
||||
await expect(page.getByRole('radio', { name: '自定义项目' })).not.toBeChecked();
|
||||
await page.getByRole('radio', { name: '小程序' }).click();
|
||||
await expect(page.getByRole('radio', { name: '小程序' })).toBeChecked();
|
||||
await page.getByRole('radio', { name: '小游戏' }).click();
|
||||
await expect(page.getByRole('radio', { name: '直接使用所选文件夹(默认)' })).toBeChecked();
|
||||
await page.getByRole('button', { name: '选择路径' }).click();
|
||||
await expect(page.getByLabel('项目路径')).toHaveValue(parentPath);
|
||||
await page.getByRole('button', { name: '确认创建' }).click();
|
||||
|
||||
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-nav-publish')).toHaveCount(0);
|
||||
await expect(page.getByText(/新项目还没有伙伴/)).toHaveCount(0);
|
||||
await expect(page.getByTestId('project-configuration-actions')).toBeVisible();
|
||||
const actionBarBottomGap = await page.getByTestId('project-configuration-actions').evaluate((element) => window.innerHeight - element.getBoundingClientRect().bottom);
|
||||
expect(actionBarBottomGap).toBeLessThan(40);
|
||||
await expect(page.getByRole('button', { name: '创建伙伴' })).toBeVisible();
|
||||
|
||||
for (const [resourceId, title] of [
|
||||
['resource-card-models', '大脑(大语言模型)'],
|
||||
['resource-card-knowledge', '笔记本(知识库)'],
|
||||
['resource-card-skills', '工具箱(技能)'],
|
||||
] as const) {
|
||||
await page.getByTestId(resourceId).click();
|
||||
const drawer = page.getByRole('dialog', { name: title });
|
||||
await expect(drawer).toBeVisible();
|
||||
if (resourceId === 'resource-card-models') {
|
||||
await expect(drawer.getByRole('combobox')).toHaveCount(0);
|
||||
await expect(drawer.locator('[data-testid="project-model-list"], [data-testid="project-model-empty-state"]')).toHaveCount(1);
|
||||
}
|
||||
await page.getByRole('button', { name: '关闭' }).click();
|
||||
await expect(page.getByRole('dialog', { name: title })).toHaveCount(0);
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
await mkdir(path.join(parentPath, '.niancode'), { recursive: true });
|
||||
await writeFile(path.join(parentPath, '.niancode', 'project.json'), JSON.stringify({
|
||||
...createProjectConfig(undefined, 'mini_game'),
|
||||
initialized: true,
|
||||
agents: [{
|
||||
id: 'e2e-partner',
|
||||
avatarId: 'avatar-01',
|
||||
roleName: '项目伙伴',
|
||||
name: 'E2E伙伴',
|
||||
builtIn: false,
|
||||
enabled: true,
|
||||
model: 'moonshot/kimi-k2.6',
|
||||
skillIds: [],
|
||||
responsibility: { mission: '验证伙伴维护弹窗。', owns: [], boundaries: [], collaborators: [], principles: [] },
|
||||
prompt: '',
|
||||
archivedAt: null,
|
||||
pinned: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}],
|
||||
}, null, 2));
|
||||
await page.reload();
|
||||
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
||||
|
||||
await page.getByTestId('resource-card-skills').click();
|
||||
await expect(page.getByTestId('superpowers-card')).toHaveCount(0);
|
||||
await expect(page.getByRole('switch', { name: '启用 Superpowers' })).toHaveCount(0);
|
||||
await expect(page.getByTestId('skill-card-pm-project-plan')).toBeVisible();
|
||||
await page.getByTestId('skill-card-pm-project-plan').click();
|
||||
await expect(page.getByTestId('skill-detail-view')).toBeVisible();
|
||||
await expect(page.getByTestId('skill-structure')).toContainText('references/guide.md');
|
||||
await expect(page.getByTestId('skill-main-file')).toContainText('# Project planning');
|
||||
await page.getByRole('button', { name: '返回技能列表' }).click();
|
||||
await expect(page.getByTestId('skill-card-pm-project-plan')).toBeVisible();
|
||||
await page.getByRole('button', { name: '关闭' }).click();
|
||||
await expect(page.getByTestId('resource-card-publish')).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '一键提交审核' })).toBeVisible();
|
||||
|
||||
const agentCard = page.getByTestId('project-agent-e2e-partner');
|
||||
await expect(agentCard).toContainText('E2E伙伴');
|
||||
await agentCard.click();
|
||||
const maintenanceDialog = page.getByRole('dialog', { name: 'E2E伙伴 · 伙伴维护' });
|
||||
await expect(maintenanceDialog).toBeVisible();
|
||||
await expect(maintenanceDialog.getByLabel(/伙伴名称/)).toHaveValue('E2E伙伴');
|
||||
await expect(page.getByText('完成配置')).toHaveCount(0);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
await rm(parentPath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
|
||||
test.describe('Project-level Superpowers setting', () => {
|
||||
test('starts publishable projects with Superpowers disabled and only the one-click release entry', async ({ launchElectronApp }) => {
|
||||
const parentPath = await mkdtemp(path.join(tmpdir(), 'niancode-superpowers-e2e-'));
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
|
||||
try {
|
||||
await app.evaluate(({ ipcMain }, selectedPath) => {
|
||||
ipcMain.removeHandler('dialog:open');
|
||||
ipcMain.handle('dialog:open', async () => ({ canceled: false, filePaths: [selectedPath] }));
|
||||
}, parentPath);
|
||||
|
||||
const page = await getStableWindow(app);
|
||||
await page.getByTestId('sidebar-create-project').click();
|
||||
await expect(page.getByRole('radio', { name: '小游戏' })).toBeChecked();
|
||||
await expect(page.getByRole('radio', { name: '小程序' })).not.toBeChecked();
|
||||
await expect(page.getByRole('radio', { name: '自定义项目' })).not.toBeChecked();
|
||||
await page.getByRole('radio', { name: '小程序' }).click();
|
||||
await expect(page.getByRole('radio', { name: '小程序' })).toBeChecked();
|
||||
await page.getByRole('radio', { name: '小游戏' }).click();
|
||||
await expect(page.getByRole('radio', { name: '直接使用所选文件夹(默认)' })).toBeChecked();
|
||||
await page.getByRole('button', { name: '选择路径' }).click();
|
||||
await expect(page.getByLabel('项目路径')).toHaveValue(parentPath);
|
||||
await page.getByRole('button', { name: '确认创建' }).click();
|
||||
|
||||
await expect(page.getByTestId('project-configuration-page')).toBeVisible();
|
||||
await expect(page.getByTestId('sidebar-nav-publish')).toHaveCount(0);
|
||||
await expect(page.getByText(/新项目还没有联系人/)).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '新增伙伴' })).toBeVisible();
|
||||
await page.getByTestId('resource-card-skills').click();
|
||||
await expect(page.getByRole('switch', { name: '启用 Superpowers' })).toHaveAttribute('aria-checked', 'false');
|
||||
await expect(page.getByText('已关闭')).toBeVisible();
|
||||
await page.getByRole('button', { name: '关闭' }).click();
|
||||
await expect(page.getByTestId('resource-card-publish')).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '一键提交审核' })).toHaveCount(1);
|
||||
await expect(page.getByText('自动部署', { exact: false })).toHaveCount(0);
|
||||
await expect(page.getByText('部署发布检查', { exact: false })).toHaveCount(0);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
await rm(parentPath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { completeSetup, expect, test } from './fixtures/electron';
|
||||
import { completeSetup, expect, openModelsPage, test } from './fixtures/electron';
|
||||
|
||||
const TEST_PROVIDER_ID = 'moonshot-e2e';
|
||||
const TEST_PROVIDER_LABEL = 'Moonshot E2E';
|
||||
@@ -6,16 +6,29 @@ const TEST_PROVIDER_LABEL = 'Moonshot E2E';
|
||||
async function seedTestProvider(page: Parameters<typeof completeSetup>[0]): Promise<void> {
|
||||
await page.evaluate(async ({ providerId, providerLabel }) => {
|
||||
const now = new Date().toISOString();
|
||||
await window.electron.ipcRenderer.invoke('provider:save', {
|
||||
id: providerId,
|
||||
name: providerLabel,
|
||||
type: 'moonshot',
|
||||
baseUrl: 'https://api.moonshot.cn/v1',
|
||||
model: 'kimi-k2.6',
|
||||
enabled: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
const result = await window.electron.ipcRenderer.invoke('hostapi:fetch', {
|
||||
path: '/api/provider-accounts',
|
||||
method: 'POST',
|
||||
body: {
|
||||
account: {
|
||||
id: providerId,
|
||||
vendorId: 'moonshot',
|
||||
label: providerLabel,
|
||||
authMode: 'api_key',
|
||||
baseUrl: 'https://api.moonshot.cn/v1',
|
||||
model: 'kimi-k2.6',
|
||||
enabled: true,
|
||||
isDefault: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
apiKey: 'sk-e2e-provider-key',
|
||||
},
|
||||
});
|
||||
|
||||
if (!result?.ok || !result.data?.ok || result.data.json?.success !== true) {
|
||||
throw new Error(`Failed to seed provider: ${JSON.stringify(result)}`);
|
||||
}
|
||||
}, { providerId: TEST_PROVIDER_ID, providerLabel: TEST_PROVIDER_LABEL });
|
||||
}
|
||||
|
||||
@@ -24,8 +37,7 @@ test.describe('NianCode provider lifecycle', () => {
|
||||
await completeSetup(page);
|
||||
await seedTestProvider(page);
|
||||
|
||||
await page.getByTestId('sidebar-nav-models').click();
|
||||
await expect(page.getByTestId('providers-settings')).toBeVisible();
|
||||
await openModelsPage(page);
|
||||
await expect(page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toContainText(TEST_PROVIDER_LABEL);
|
||||
|
||||
await page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`).hover();
|
||||
@@ -39,7 +51,7 @@ test.describe('NianCode provider lifecycle', () => {
|
||||
await completeSetup(page);
|
||||
await seedTestProvider(page);
|
||||
|
||||
await page.getByTestId('sidebar-nav-models').click();
|
||||
await openModelsPage(page);
|
||||
await expect(page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toContainText(TEST_PROVIDER_LABEL);
|
||||
|
||||
await page.getByTestId(`provider-card-${TEST_PROVIDER_ID}`).hover();
|
||||
@@ -52,10 +64,11 @@ test.describe('NianCode provider lifecycle', () => {
|
||||
try {
|
||||
const relaunchedPage = await relaunchedApp.firstWindow();
|
||||
await relaunchedPage.waitForLoadState('domcontentloaded');
|
||||
await expect(relaunchedPage.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
await relaunchedPage.getByTestId('ai-module-option-programming').click();
|
||||
await expect(relaunchedPage.getByTestId('main-layout')).toBeVisible();
|
||||
|
||||
await relaunchedPage.getByTestId('sidebar-nav-models').click();
|
||||
await expect(relaunchedPage.getByTestId('providers-settings')).toBeVisible();
|
||||
await openModelsPage(relaunchedPage);
|
||||
await expect(relaunchedPage.getByTestId(`provider-card-${TEST_PROVIDER_ID}`)).toHaveCount(0);
|
||||
await expect(relaunchedPage.getByText(TEST_PROVIDER_LABEL)).toHaveCount(0);
|
||||
} finally {
|
||||
@@ -147,8 +160,7 @@ test.describe('NianCode provider lifecycle', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await page.getByTestId('sidebar-nav-models').click();
|
||||
await expect(page.getByTestId('providers-settings')).toBeVisible();
|
||||
await openModelsPage(page);
|
||||
|
||||
await page.getByTestId('providers-add-button').click();
|
||||
await expect(page.getByTestId('add-provider-dialog')).toBeVisible();
|
||||
@@ -160,7 +172,7 @@ test.describe('NianCode provider lifecycle', () => {
|
||||
await page.getByTestId('add-provider-model-id-input').fill('local-model');
|
||||
await page.getByTestId('add-provider-submit-button').click();
|
||||
|
||||
await expect(page.getByTestId('provider-card-custom')).toContainText('LM Studio Local');
|
||||
await expect(page.getByTestId(/^provider-card-custom-/).filter({ hasText: 'LM Studio Local' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('edit form validates the new API key inline before saving (single button)', async ({ electronApp, page }) => {
|
||||
@@ -223,8 +235,7 @@ test.describe('NianCode provider lifecycle', () => {
|
||||
});
|
||||
});
|
||||
|
||||
await page.getByTestId('sidebar-nav-models').click();
|
||||
await expect(page.getByTestId('providers-settings')).toBeVisible();
|
||||
await openModelsPage(page);
|
||||
await expect(page.getByTestId('provider-card-moonshot-edit')).toBeVisible();
|
||||
|
||||
await page.getByTestId('provider-card-moonshot-edit').hover();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { closeElectronApp, expect, getStableWindow, test } from './fixtures/electron';
|
||||
import { closeElectronApp, expect, getStableWindow, openModelsPage, test } from './fixtures/electron';
|
||||
|
||||
test.describe('hover-only scrollbar visibility', () => {
|
||||
test('hides scrollbars until a scroll container is hovered', async ({ launchElectronApp }) => {
|
||||
@@ -6,8 +6,9 @@ test.describe('hover-only scrollbar visibility', () => {
|
||||
|
||||
try {
|
||||
const page = await getStableWindow(app);
|
||||
await page.getByTestId('sidebar-nav-models').click();
|
||||
await expect(page.getByTestId('models-page')).toBeVisible();
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await openModelsPage(page);
|
||||
|
||||
const scrollContainer = page.locator('[data-testid="models-page"] .overflow-y-auto').first();
|
||||
await expect(scrollContainer).toBeVisible();
|
||||
|
||||
@@ -27,6 +27,7 @@ test.describe('NianCode developer proxy settings', () => {
|
||||
|
||||
await openSettingsFromSidebarAccountMenu(page);
|
||||
await expect(page.getByTestId('settings-page')).toBeVisible();
|
||||
await page.getByTestId('settings-reveal-additional').click();
|
||||
|
||||
const devModeToggle = page.getByTestId('settings-dev-mode-switch');
|
||||
await expect(devModeToggle).toBeVisible();
|
||||
|
||||
@@ -28,6 +28,8 @@ test.describe('NianCode window zoom shortcuts', () => {
|
||||
|
||||
try {
|
||||
const page = await getStableWindow(app);
|
||||
await expect(page.getByTestId('ai-module-selection-page')).toBeVisible();
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
|
||||
await app.evaluate(({ BrowserWindow }) => {
|
||||
|
||||
Reference in New Issue
Block a user