project-knowledge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/INDEX.md +53 -0
- package/README.md +79 -0
- package/_site/README.md +63 -0
- package/_site/_test/ai-profile-test.js +199 -0
- package/_site/_test/baseline-schema-test.js +132 -0
- package/_site/_test/commit-analysis-test.js +184 -0
- package/_site/_test/context-pack-test.js +199 -0
- package/_site/_test/draft-apply-test.js +363 -0
- package/_site/_test/git-validation-test.js +171 -0
- package/_site/_test/hook-trigger-test.js +257 -0
- package/_site/_test/initial-analysis-test.js +228 -0
- package/_site/_test/job-orchestrator-test.js +297 -0
- package/_site/_test/kb-v2-templates-test.js +189 -0
- package/_site/_test/pr-consumer-contract-test.js +236 -0
- package/_site/_test/run-all-tests.js +135 -0
- package/_site/_test/scanner-test.js +206 -0
- package/_site/_test/ui-smoke-test.js +237 -0
- package/_site/_test/ui-test.js +237 -0
- package/_site/index.html +1166 -0
- package/_site/lib/ai-adapter.js +287 -0
- package/_site/lib/analysis-orchestrator.js +433 -0
- package/_site/lib/context-pack-builder.js +290 -0
- package/_site/lib/draft-apply.js +219 -0
- package/_site/lib/git-runner.js +26 -0
- package/_site/lib/hook-manager.js +148 -0
- package/_site/lib/job-orchestrator.js +231 -0
- package/_site/lib/kb-validator.js +224 -0
- package/_site/lib/llm-client.js +126 -0
- package/_site/lib/scanner.js +94 -0
- package/_site/scripts/hook-trigger.js +133 -0
- package/_site/scripts/safe-runner.js +151 -0
- package/_site/server.js +1058 -0
- package/_site/start.bat +26 -0
- package/_site/stop.bat +11 -0
- package/ai-profiles.json +18 -0
- package/docs/ai-knowledge-base-system-design.md +395 -0
- package/docs/pr-consumer-contract.md +198 -0
- package/docs/project-goal.md +72 -0
- package/docs/project-registry-schema.md +46 -0
- package/docs/testing-strategy.md +169 -0
- package/iterations.json +23 -0
- package/package.json +47 -0
- package/scripts/gen-commit-doc.ps1 +178 -0
- package/scripts/gen-commit-doc.sh +197 -0
- package/scripts/list-features.ps1 +41 -0
- package/scripts/register-scheduled-task.bat +5 -0
- package/templates/change.md +59 -0
- package/templates/commit-feature.md +56 -0
- package/templates/feature.md +44 -0
- package/templates/framework.md +80 -0
- package/templates/index-header.md +3 -0
- package/templates/kb-manifest.json +38 -0
- package/templates/module.md +58 -0
- package/templates/project-analysis.md +48 -0
- package/templates/project-goal.md +55 -0
- package/templates/project-readme.md +60 -0
- package/templates/quality-review-rules.md +37 -0
- package/templates/update-entry.md +7 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// UI smoke test - runs in a real Chromium via CDP.
|
|
2
|
+
//
|
|
3
|
+
// It verifies the backend-driven control center renders, core navigation is
|
|
4
|
+
// visible, project status data appears, tab switching works, and no console or
|
|
5
|
+
// network errors are emitted during load.
|
|
6
|
+
//
|
|
7
|
+
// Run: node _site/_test/ui-smoke-test.js [URL]
|
|
8
|
+
|
|
9
|
+
const { spawn } = require('child_process');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const http = require('http');
|
|
13
|
+
const WebSocket = require('ws');
|
|
14
|
+
|
|
15
|
+
const CHROME = 'C:\\Users\\SanQian\\AppData\\Local\\ms-playwright\\chromium-1223\\chrome-win64\\chrome.exe';
|
|
16
|
+
const TARGET_URL = process.argv[2] || 'http://127.0.0.1:7777/';
|
|
17
|
+
const OUT_DIR = path.join(__dirname, 'ui-screenshots');
|
|
18
|
+
const PROFILE = path.join(OUT_DIR, 'smoke-profile');
|
|
19
|
+
const PORT = 9340;
|
|
20
|
+
|
|
21
|
+
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
22
|
+
fs.rmSync(PROFILE, { recursive: true, force: true });
|
|
23
|
+
fs.mkdirSync(PROFILE, { recursive: true });
|
|
24
|
+
|
|
25
|
+
function fetchJson(url) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
http.get(url, res => {
|
|
28
|
+
let body = '';
|
|
29
|
+
res.on('data', chunk => body += chunk);
|
|
30
|
+
res.on('end', () => {
|
|
31
|
+
try { resolve(JSON.parse(body)); }
|
|
32
|
+
catch (e) { reject(e); }
|
|
33
|
+
});
|
|
34
|
+
}).on('error', reject);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function waitFor(fn, label, ms = 15000) {
|
|
39
|
+
const deadline = Date.now() + ms;
|
|
40
|
+
let last;
|
|
41
|
+
while (Date.now() < deadline) {
|
|
42
|
+
try {
|
|
43
|
+
const result = await fn();
|
|
44
|
+
if (result) return result;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
last = e;
|
|
47
|
+
}
|
|
48
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
49
|
+
}
|
|
50
|
+
throw new Error('timeout: ' + label + ' :: ' + (last && last.message));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assert(cond, msg) {
|
|
54
|
+
if (!cond) throw new Error('ASSERT: ' + msg);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
(async () => {
|
|
58
|
+
const child = spawn(CHROME, [
|
|
59
|
+
'--headless=new',
|
|
60
|
+
'--disable-gpu',
|
|
61
|
+
'--no-sandbox',
|
|
62
|
+
'--disable-dev-shm-usage',
|
|
63
|
+
'--remote-debugging-port=' + PORT,
|
|
64
|
+
'--user-data-dir=' + PROFILE,
|
|
65
|
+
'--window-size=1280,900',
|
|
66
|
+
'about:blank',
|
|
67
|
+
], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
68
|
+
child.stderr.on('data', () => {});
|
|
69
|
+
|
|
70
|
+
const errors = [];
|
|
71
|
+
const warnings = [];
|
|
72
|
+
const requestFailures = [];
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const pages = await waitFor(() => fetchJson(`http://127.0.0.1:${PORT}/json/list`), 'list');
|
|
76
|
+
const page = pages.find(p => p.type === 'page') || pages[0];
|
|
77
|
+
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
|
78
|
+
await new Promise((resolve, reject) => {
|
|
79
|
+
ws.on('open', resolve);
|
|
80
|
+
ws.on('error', reject);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
let nextId = 1;
|
|
84
|
+
const pending = new Map();
|
|
85
|
+
ws.on('message', m => {
|
|
86
|
+
const msg = JSON.parse(m.toString());
|
|
87
|
+
if (msg.id && pending.has(msg.id)) {
|
|
88
|
+
const { resolve, reject } = pending.get(msg.id);
|
|
89
|
+
pending.delete(msg.id);
|
|
90
|
+
if (msg.error) reject(new Error(msg.error.message));
|
|
91
|
+
else resolve(msg.result);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (msg.method === 'Runtime.consoleAPICalled') {
|
|
96
|
+
const type = msg.params.type;
|
|
97
|
+
const text = (msg.params.args || []).map(arg => arg.value !== undefined ? arg.value : arg.description).join(' ');
|
|
98
|
+
if (/cdn\.tailwindcss\.com should not be used in production/.test(text)) return;
|
|
99
|
+
if (type === 'error') errors.push(text);
|
|
100
|
+
else if (type === 'warning') warnings.push(text);
|
|
101
|
+
} else if (msg.method === 'Network.loadingFailed') {
|
|
102
|
+
requestFailures.push(msg.params);
|
|
103
|
+
} else if (msg.method === 'Network.responseReceived' && msg.params.response.status >= 500) {
|
|
104
|
+
requestFailures.push({ url: msg.params.response.url, status: msg.params.response.status });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
function send(method, params = {}) {
|
|
109
|
+
const id = nextId++;
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
pending.set(id, { resolve, reject });
|
|
112
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await send('Runtime.enable');
|
|
117
|
+
await send('Page.enable');
|
|
118
|
+
await send('Network.enable');
|
|
119
|
+
|
|
120
|
+
await send('Page.navigate', { url: TARGET_URL });
|
|
121
|
+
await waitFor(async () => {
|
|
122
|
+
const r = await send('Runtime.evaluate', {
|
|
123
|
+
expression: 'document.readyState === "complete" && document.querySelector("#app") && /Control Center|控制中心/.test(document.querySelector("#app").innerText)',
|
|
124
|
+
returnByValue: true,
|
|
125
|
+
});
|
|
126
|
+
return r.result.value === true;
|
|
127
|
+
}, '#app to render control center', 15000);
|
|
128
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
129
|
+
|
|
130
|
+
let r = await send('Runtime.evaluate', {
|
|
131
|
+
expression: 'document.querySelector("#app") && document.querySelector("#app").innerHTML.trim()',
|
|
132
|
+
returnByValue: true,
|
|
133
|
+
});
|
|
134
|
+
const appHtml = r.result.value;
|
|
135
|
+
assert(appHtml && appHtml.length > 500, `appHtml too short: ${(appHtml || '').length}`);
|
|
136
|
+
assert(!appHtml.includes('<!---->') || appHtml.indexOf('header') >= 0, 'app rendered as comment');
|
|
137
|
+
|
|
138
|
+
r = await send('Runtime.evaluate', {
|
|
139
|
+
expression: 'document.querySelector("h1") ? document.querySelector("h1").innerText : ""',
|
|
140
|
+
returnByValue: true,
|
|
141
|
+
});
|
|
142
|
+
assert(/Project Supervision|项目监督/.test(r.result.value), 'header missing');
|
|
143
|
+
|
|
144
|
+
r = await send('Runtime.evaluate', {
|
|
145
|
+
expression: 'Array.from(document.querySelectorAll("button, a")).map(e => e.innerText.trim()).filter(Boolean)',
|
|
146
|
+
returnByValue: true,
|
|
147
|
+
});
|
|
148
|
+
const navText = r.result.value;
|
|
149
|
+
assert(navText.some(t => /^Dashboard|^仪表盘/.test(t)), 'Dashboard nav missing');
|
|
150
|
+
assert(navText.some(t => /^Import|^导入/.test(t)), 'Import nav missing');
|
|
151
|
+
assert(navText.some(t => /^AI Profiles|^AI 配置/.test(t)), 'AI Profiles nav missing');
|
|
152
|
+
assert(navText.some(t => /^Runs \/ Drafts|^运行 \/ 草稿/.test(t)), 'Runs / Drafts nav missing');
|
|
153
|
+
assert(navText.some(t => /^Schedule|^定时任务/.test(t)), 'Schedule nav missing');
|
|
154
|
+
assert(navText.some(t => /^Logs|^日志/.test(t)), 'Logs nav missing');
|
|
155
|
+
|
|
156
|
+
r = await send('Runtime.evaluate', {
|
|
157
|
+
expression: 'document.body.innerText',
|
|
158
|
+
returnByValue: true,
|
|
159
|
+
});
|
|
160
|
+
const bodyText = r.result.value;
|
|
161
|
+
assert(/Project Operations|项目操作/.test(bodyText), 'project operations panel missing');
|
|
162
|
+
assert(/Running Jobs|运行中的任务/.test(bodyText), 'running jobs panel missing');
|
|
163
|
+
assert(/Repo|仓库/.test(bodyText), 'repo status missing');
|
|
164
|
+
|
|
165
|
+
r = await send('Runtime.evaluate', {
|
|
166
|
+
expression: 'getComputedStyle(document.querySelector("aside")).backgroundColor',
|
|
167
|
+
returnByValue: true,
|
|
168
|
+
});
|
|
169
|
+
assert(!/^rgba?\(255,\s*255,\s*255/.test(r.result.value), `sidebar background is not themed: ${r.result.value}`);
|
|
170
|
+
|
|
171
|
+
r = await send('Runtime.evaluate', {
|
|
172
|
+
expression: 'Array.from(document.body.innerText.matchAll(/[0-9a-f]{7,40}/g)).map(m => m[0]).slice(0, 5)',
|
|
173
|
+
returnByValue: true,
|
|
174
|
+
});
|
|
175
|
+
assert(r.result.value.length >= 1, 'no commit hash rendered');
|
|
176
|
+
|
|
177
|
+
await send('Page.reload', { ignoreCache: true });
|
|
178
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
179
|
+
r = await send('Runtime.evaluate', {
|
|
180
|
+
expression: '/Project Supervision|项目监督/.test(document.body.innerText) && /Project Operations|项目操作/.test(document.body.innerText)',
|
|
181
|
+
returnByValue: true,
|
|
182
|
+
});
|
|
183
|
+
assert(r.result.value, 'after reload dashboard did not render');
|
|
184
|
+
|
|
185
|
+
r = await send('Runtime.evaluate', {
|
|
186
|
+
expression: '(() => { const btn = Array.from(document.querySelectorAll("button, a")).find(b => /^Schedule|^定时任务/.test(b.innerText)); if (btn) btn.click(); return btn ? btn.innerText : "NO BTN"; })()',
|
|
187
|
+
returnByValue: true,
|
|
188
|
+
});
|
|
189
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
190
|
+
r = await send('Runtime.evaluate', {
|
|
191
|
+
expression: '/Schedule|定时任务/.test(document.body.innerText) && /Controls|控制/.test(document.body.innerText)',
|
|
192
|
+
returnByValue: true,
|
|
193
|
+
});
|
|
194
|
+
assert(r.result.value, 'Schedule tab did not render its content');
|
|
195
|
+
|
|
196
|
+
r = await send('Runtime.evaluate', {
|
|
197
|
+
expression: '(() => { const btn = Array.from(document.querySelectorAll("button, a")).find(b => /Runs \\/ Drafts|运行 \\/ 草稿/.test(b.innerText)); if (btn) btn.click(); return btn ? btn.innerText : "NO BTN"; })()',
|
|
198
|
+
returnByValue: true,
|
|
199
|
+
});
|
|
200
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
201
|
+
r = await send('Runtime.evaluate', {
|
|
202
|
+
expression: '/Runs|运行/.test(document.body.innerText) && /Drafts|草稿/.test(document.body.innerText)',
|
|
203
|
+
returnByValue: true,
|
|
204
|
+
});
|
|
205
|
+
assert(r.result.value, 'Runs / Drafts tab did not render its content');
|
|
206
|
+
|
|
207
|
+
r = await send('Runtime.evaluate', {
|
|
208
|
+
expression: '(() => { const btn = Array.from(document.querySelectorAll("button, a")).find(b => /^AI Profiles|^AI 配置/.test(b.innerText)); if (btn) btn.click(); return btn ? btn.innerText : "NO BTN"; })()',
|
|
209
|
+
returnByValue: true,
|
|
210
|
+
});
|
|
211
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
212
|
+
r = await send('Runtime.evaluate', {
|
|
213
|
+
expression: '/Knowledge output language|知识库输出语言/.test(document.body.innerText)',
|
|
214
|
+
returnByValue: true,
|
|
215
|
+
});
|
|
216
|
+
assert(r.result.value, 'AI language setting did not render');
|
|
217
|
+
|
|
218
|
+
const shot = await send('Page.captureScreenshot', { format: 'png', fullPage: true });
|
|
219
|
+
fs.writeFileSync(path.join(OUT_DIR, 'smoke-final.png'), Buffer.from(shot.data, 'base64'));
|
|
220
|
+
|
|
221
|
+
assert(errors.length === 0, `console errors: ${JSON.stringify(errors, null, 2)}`);
|
|
222
|
+
assert(requestFailures.length === 0, `request failures: ${JSON.stringify(requestFailures, null, 2)}`);
|
|
223
|
+
|
|
224
|
+
console.log('UI smoke test passed');
|
|
225
|
+
console.log(' - nav items:', navText.length);
|
|
226
|
+
console.log(' - warnings:', warnings.length);
|
|
227
|
+
console.log(' - errors:', errors.length);
|
|
228
|
+
ws.close();
|
|
229
|
+
} catch (e) {
|
|
230
|
+
console.error('UI smoke test failed:', e.message);
|
|
231
|
+
if (errors.length) console.error('console errors:', JSON.stringify(errors, null, 2));
|
|
232
|
+
if (requestFailures.length) console.error('request failures:', JSON.stringify(requestFailures, null, 2));
|
|
233
|
+
process.exitCode = 1;
|
|
234
|
+
} finally {
|
|
235
|
+
try { child.kill(); } catch {}
|
|
236
|
+
}
|
|
237
|
+
})();
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// Playwright UI test for KB management site
|
|
2
|
+
// Run: NODE_PATH="..." node _test/ui-test.js
|
|
3
|
+
const { chromium } = require('playwright');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
const SCREENSHOT_DIR = path.resolve(__dirname, 'screenshots');
|
|
8
|
+
const REPORT_PATH = path.resolve(__dirname, 'ui-test-report.json');
|
|
9
|
+
const URL = 'http://localhost:7777/';
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(SCREENSHOT_DIR)) fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
12
|
+
|
|
13
|
+
const results = [];
|
|
14
|
+
let stepNum = 0;
|
|
15
|
+
|
|
16
|
+
function record(name, status, details) {
|
|
17
|
+
stepNum++;
|
|
18
|
+
const entry = { step: stepNum, name, status, ...details };
|
|
19
|
+
results.push(entry);
|
|
20
|
+
const icon = status === 'PASS' ? '✓' : (status === 'FAIL' ? '✗' : '⚠');
|
|
21
|
+
console.log(` ${icon} [${status}] ${name}${details.note ? ' — ' + details.note : ''}`);
|
|
22
|
+
return entry;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function shot(page, name) {
|
|
26
|
+
const filename = `${String(stepNum).padStart(2, '0')}-${name}.png`;
|
|
27
|
+
const filepath = path.join(SCREENSHOT_DIR, filename);
|
|
28
|
+
await page.screenshot({ path: filepath, fullPage: true });
|
|
29
|
+
return filepath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
(async () => {
|
|
33
|
+
console.log('============================================');
|
|
34
|
+
console.log(' UI TEST — Playwright + Chromium headless');
|
|
35
|
+
console.log('============================================\n');
|
|
36
|
+
const browser = await chromium.launch({ headless: true });
|
|
37
|
+
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
|
38
|
+
const page = await context.newPage();
|
|
39
|
+
|
|
40
|
+
// Track dialogs without auto-accepting (we want to verify the prompts, not click through them)
|
|
41
|
+
// Default: dismiss dialogs so test actions are non-destructive
|
|
42
|
+
page.on('dialog', d => d.dismiss().catch(() => {}));
|
|
43
|
+
|
|
44
|
+
// ==== Initial load ====
|
|
45
|
+
console.log('▶ Initial load');
|
|
46
|
+
await page.goto(URL, { waitUntil: 'networkidle' });
|
|
47
|
+
await page.waitForSelector('#app article', { timeout: 5000 });
|
|
48
|
+
const initialShot = await shot(page, '01-projects-initial');
|
|
49
|
+
const projectCount = await page.locator('#app article').count();
|
|
50
|
+
record('Page loads with project cards', projectCount === 6 ? 'PASS' : 'FAIL', {
|
|
51
|
+
note: `${projectCount} cards rendered`,
|
|
52
|
+
screenshot: initialShot,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ==== Verify tokenrank-cloud card is present ====
|
|
56
|
+
const tclCard = page.locator('article:has-text("TokenRank Cloud")');
|
|
57
|
+
const tclVisible = await tclCard.isVisible();
|
|
58
|
+
record('TokenRank Cloud card visible', tclVisible ? 'PASS' : 'FAIL', {
|
|
59
|
+
note: tclVisible ? 'card found' : 'card not found',
|
|
60
|
+
screenshot: initialShot,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ==== Verify "KB ready" badge for tokenrank-cloud ====
|
|
64
|
+
const tclKbBadge = await tclCard.locator('text=✓ KB ready').count();
|
|
65
|
+
record('TokenRank Cloud shows "KB ready" badge', tclKbBadge > 0 ? 'PASS' : 'FAIL', {
|
|
66
|
+
note: tclKbBadge > 0 ? 'badge present' : 'badge missing',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ==== Click "View tree" on tokenrank-cloud ====
|
|
70
|
+
console.log('\n▶ Projects tab — View tree');
|
|
71
|
+
const viewTreeBtn = tclCard.locator('button:has-text("View tree")');
|
|
72
|
+
await viewTreeBtn.click();
|
|
73
|
+
await page.waitForTimeout(500);
|
|
74
|
+
const treeDetails = tclCard.locator('details');
|
|
75
|
+
const treeSummary = await treeDetails.locator('summary').textContent();
|
|
76
|
+
const treeItems = await tclCard.locator('details li').count();
|
|
77
|
+
const treeShot = await shot(page, '02-tree-expanded');
|
|
78
|
+
record('View tree expands project tree', treeItems > 0 ? 'PASS' : 'FAIL', {
|
|
79
|
+
note: `${treeItems} entries (summary: ${treeSummary})`,
|
|
80
|
+
screenshot: treeShot,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ==== Close the tree ====
|
|
84
|
+
await viewTreeBtn.click();
|
|
85
|
+
await page.waitForTimeout(200);
|
|
86
|
+
|
|
87
|
+
// ==== "+ Add" tab ====
|
|
88
|
+
console.log('\n▶ + Add tab');
|
|
89
|
+
await page.click('button:has-text("+ Add")');
|
|
90
|
+
await page.waitForSelector('form');
|
|
91
|
+
const addShot1 = await shot(page, '03-add-empty');
|
|
92
|
+
record('+ Add tab shows form', true, { note: 'form visible', screenshot: addShot1 });
|
|
93
|
+
|
|
94
|
+
// ==== Fill the form with a test entry ====
|
|
95
|
+
await page.fill('input[placeholder="my-new-project"]', 'ui-test-proj');
|
|
96
|
+
await page.fill('input[placeholder="My New Project"]', 'UI Test Project');
|
|
97
|
+
await page.fill('input[placeholder^="D:\\\\SanQian.Xu"]', 'D:\\SanQian.Xu\\UI-Test-Project');
|
|
98
|
+
await page.fill('input[placeholder="TypeScript"]', 'TypeScript');
|
|
99
|
+
await page.fill('input[placeholder="react, vite, api"]', 'ui, test, demo');
|
|
100
|
+
const filledShot = await shot(page, '04-add-filled');
|
|
101
|
+
record('Form fields fill correctly', true, { note: 'all fields filled', screenshot: filledShot });
|
|
102
|
+
|
|
103
|
+
// ==== Reset button ====
|
|
104
|
+
await page.click('button:has-text("Reset")');
|
|
105
|
+
await page.waitForTimeout(200);
|
|
106
|
+
const slugValue = await page.inputValue('input[placeholder="my-new-project"]');
|
|
107
|
+
record('Reset button clears form', slugValue === '' ? 'PASS' : 'FAIL', {
|
|
108
|
+
note: 'slug input is empty',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ==== Schedule & Run tab ====
|
|
112
|
+
console.log('\n▶ Schedule & Run tab');
|
|
113
|
+
await page.click('button:has-text("Schedule & Run")');
|
|
114
|
+
await page.waitForSelector('text=Current status');
|
|
115
|
+
const schedShot1 = await shot(page, '05-schedule-initial');
|
|
116
|
+
const stateText = await page.locator('div.flex.justify-between:has(span:text("State:"))').last().locator('span').nth(1).textContent().catch(() => '?');
|
|
117
|
+
const nextRunText = await page.locator('div.flex.justify-between:has(span:text("Next run:"))').last().locator('span').nth(1).textContent().catch(() => '?');
|
|
118
|
+
record('Schedule card shows current state', stateText.includes('Ready') ? 'PASS' : 'FAIL', {
|
|
119
|
+
note: `state=${stateText.trim()}, nextRun=${nextRunText.trim()}`,
|
|
120
|
+
screenshot: schedShot1,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ==== Change frequency to hourly ====
|
|
124
|
+
await page.selectOption('select', 'hourly');
|
|
125
|
+
await page.waitForTimeout(200);
|
|
126
|
+
const hourlyShot = await shot(page, '06-frequency-hourly');
|
|
127
|
+
record('Frequency dropdown accepts "Hourly"', true, { note: 'selected', screenshot: hourlyShot });
|
|
128
|
+
|
|
129
|
+
// ==== Click Apply ====
|
|
130
|
+
await page.click('button:has-text("Apply")');
|
|
131
|
+
await page.waitForSelector('text=Schedule updated', { timeout: 5000 }).catch(() => {});
|
|
132
|
+
await page.waitForTimeout(500);
|
|
133
|
+
const appliedShot = await shot(page, '07-schedule-applied');
|
|
134
|
+
const appliedMsg = await page.locator('text=Schedule updated').count();
|
|
135
|
+
record('Apply button updates schedule', appliedMsg > 0 ? 'PASS' : 'FAIL', {
|
|
136
|
+
note: appliedMsg > 0 ? 'success message shown' : 'no success message',
|
|
137
|
+
screenshot: appliedShot,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ==== Verify state changed ====
|
|
141
|
+
await page.waitForTimeout(800);
|
|
142
|
+
const stateAfter = await page.locator('div.flex.justify-between:has(span:text("State:"))').last().locator('span').nth(1).textContent().catch(() => '?');
|
|
143
|
+
record('Schedule state remains Ready after change', stateAfter.includes('Ready') ? 'PASS' : 'FAIL', {
|
|
144
|
+
note: `state=${stateAfter.trim()}`,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ==== Restore: change back to Daily 08:00 ====
|
|
148
|
+
await page.selectOption('select', 'daily');
|
|
149
|
+
await page.waitForTimeout(200);
|
|
150
|
+
// Set time to 08:00
|
|
151
|
+
await page.fill('input[type="time"]', '08:00');
|
|
152
|
+
await page.click('button:has-text("Apply")');
|
|
153
|
+
await page.waitForTimeout(1500);
|
|
154
|
+
const restoredShot = await shot(page, '08-schedule-restored');
|
|
155
|
+
const scheduleTypeText = await page.locator('text=Schedule Type:').locator('xpath=following-sibling::*[1]').textContent().catch(() => '?');
|
|
156
|
+
const startTimeText = await page.locator('text=Start Time:').locator('xpath=following-sibling::*[1]').textContent().catch(() => '?');
|
|
157
|
+
// Note: schedule card may not show Schedule Type / Start Time. The data is in raw map but not exposed.
|
|
158
|
+
// So just check the message
|
|
159
|
+
record('Restore daily 08:00', true, {
|
|
160
|
+
note: 'applied, schedule type=' + scheduleTypeText.trim() + ', startTime=' + startTimeText.trim(),
|
|
161
|
+
screenshot: restoredShot,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ==== Run now with tokenrank-cloud (will fail, but should display error) ====
|
|
165
|
+
console.log('\n▶ Run now');
|
|
166
|
+
await page.selectOption('select:near(:text("Project slug"))', 'tokenrank-cloud').catch(async () => {
|
|
167
|
+
// Alternative: the select for runSlug is the second one on the page
|
|
168
|
+
const selects = page.locator('select');
|
|
169
|
+
await selects.last().selectOption('tokenrank-cloud');
|
|
170
|
+
});
|
|
171
|
+
await page.waitForTimeout(200);
|
|
172
|
+
// Alternative: locate the runSlug select by label
|
|
173
|
+
const runSelect = page.locator('label:has-text("Project slug")').locator('xpath=following::select[1]');
|
|
174
|
+
await runSelect.selectOption('tokenrank-cloud').catch(() => {});
|
|
175
|
+
await page.waitForTimeout(200);
|
|
176
|
+
const runSelectedShot = await shot(page, '09-run-selected');
|
|
177
|
+
record('Run slug dropdown works', true, { screenshot: runSelectedShot });
|
|
178
|
+
|
|
179
|
+
// Click Run now
|
|
180
|
+
await page.click('button:has-text("Run now")');
|
|
181
|
+
// Wait for log tab to appear
|
|
182
|
+
await page.waitForSelector('h2:has-text("Last run output")', { timeout: 10000 });
|
|
183
|
+
await page.waitForTimeout(2000);
|
|
184
|
+
const logShot = await shot(page, '10-log-output');
|
|
185
|
+
const logOutput = await page.locator('pre').textContent();
|
|
186
|
+
const hasOutput = logOutput && logOutput.length > 50;
|
|
187
|
+
const hasError = logOutput && (logOutput.includes('fatal') || logOutput.includes('cannot change to'));
|
|
188
|
+
record('Run now executes and shows output', hasOutput ? 'PASS' : 'FAIL', {
|
|
189
|
+
note: hasError ? 'error captured (expected for broken gitPath)' : 'no error in output',
|
|
190
|
+
screenshot: logShot,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ==== Back to Projects ====
|
|
194
|
+
console.log('\n▶ Back to Projects');
|
|
195
|
+
await page.click('button:has-text("Projects")');
|
|
196
|
+
await page.waitForSelector('#app article');
|
|
197
|
+
await page.waitForTimeout(500);
|
|
198
|
+
const finalShot = await shot(page, '11-projects-final');
|
|
199
|
+
const finalCount = await page.locator('#app article').count();
|
|
200
|
+
record('Projects tab reloads with all 6 projects', finalCount === 6 ? 'PASS' : 'FAIL', {
|
|
201
|
+
note: `${finalCount} cards`,
|
|
202
|
+
screenshot: finalShot,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ==== Verify "Remove" button shows confirm dialog ====
|
|
206
|
+
// We won't actually remove, just verify the dialog appears
|
|
207
|
+
console.log('\n▶ Remove confirm dialog');
|
|
208
|
+
let capturedMsg = null;
|
|
209
|
+
page.once('dialog', d => { capturedMsg = d.message(); d.dismiss().catch(() => {}); });
|
|
210
|
+
await page.click('article:has-text("TokenRank Cloud") button:has-text("Remove")');
|
|
211
|
+
await page.waitForTimeout(800);
|
|
212
|
+
if (capturedMsg) {
|
|
213
|
+
record('Remove shows confirm dialog', capturedMsg.includes('Remove project') ? 'PASS' : 'FAIL', {
|
|
214
|
+
note: `dialog text: ${capturedMsg.slice(0, 80)}…`,
|
|
215
|
+
});
|
|
216
|
+
} else {
|
|
217
|
+
record('Remove shows confirm dialog', 'FAIL', { note: 'no dialog appeared' });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ==== Summary ====
|
|
221
|
+
await browser.close();
|
|
222
|
+
|
|
223
|
+
const pass = results.filter(r => r.status === 'PASS').length;
|
|
224
|
+
const fail = results.filter(r => r.status === 'FAIL').length;
|
|
225
|
+
const summary = { url: URL, total: results.length, pass, fail, results };
|
|
226
|
+
|
|
227
|
+
fs.writeFileSync(REPORT_PATH, JSON.stringify(summary, null, 2));
|
|
228
|
+
console.log('\n============================================');
|
|
229
|
+
console.log(` UI TEST SUMMARY: ${pass}/${results.length} passed${fail ? ', ' + fail + ' failed' : ''}`);
|
|
230
|
+
console.log('============================================');
|
|
231
|
+
console.log(` Report: ${REPORT_PATH}`);
|
|
232
|
+
console.log(` Screenshots: ${SCREENSHOT_DIR}`);
|
|
233
|
+
process.exit(fail ? 1 : 0);
|
|
234
|
+
})().catch(e => {
|
|
235
|
+
console.error('FATAL:', e);
|
|
236
|
+
process.exit(2);
|
|
237
|
+
});
|