mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
chore(ci): add frontend coverage workflow
This commit is contained in:
50
.github/workflows/frontend-tests.yml
vendored
Normal file
50
.github/workflows/frontend-tests.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: Frontend Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- 'package.json'
|
||||||
|
- 'package-lock.json'
|
||||||
|
- 'vitest.config.js'
|
||||||
|
- 'tests/frontend/**'
|
||||||
|
- 'static/js/**'
|
||||||
|
- '.github/workflows/frontend-tests.yml'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'package.json'
|
||||||
|
- 'package-lock.json'
|
||||||
|
- 'vitest.config.js'
|
||||||
|
- 'tests/frontend/**'
|
||||||
|
- 'static/js/**'
|
||||||
|
- '.github/workflows/frontend-tests.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
vitest:
|
||||||
|
name: Run Vitest with coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node.js 20
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run frontend tests with coverage
|
||||||
|
run: npm run test:coverage
|
||||||
|
|
||||||
|
- name: Upload coverage artifact
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: frontend-coverage
|
||||||
|
path: coverage/frontend
|
||||||
|
if-no-files-found: warn
|
||||||
@@ -11,7 +11,7 @@ This roadmap tracks the planned rollout of automated testing for the ComfyUI LoR
|
|||||||
| Phase 2 | Test AppCore orchestration | Simulate page bootstrapping, infinite scroll hooks, and manager registration using JSDOM DOM fixtures | ✅ Complete | AppCore initialization + page feature suites now validate manager wiring, infinite scroll hooks, and onboarding gating |
|
| Phase 2 | Test AppCore orchestration | Simulate page bootstrapping, infinite scroll hooks, and manager registration using JSDOM DOM fixtures | ✅ Complete | AppCore initialization + page feature suites now validate manager wiring, infinite scroll hooks, and onboarding gating |
|
||||||
| Phase 3 | Validate page-specific managers | Add focused suites for `loras`, `checkpoints`, `embeddings`, and `recipes` managers covering filtering, sorting, and bulk actions | ✅ Complete | LoRA/checkpoint suites expanded; embeddings + recipes managers now covered with initialization, filtering, and duplicate workflows |
|
| Phase 3 | Validate page-specific managers | Add focused suites for `loras`, `checkpoints`, `embeddings`, and `recipes` managers covering filtering, sorting, and bulk actions | ✅ Complete | LoRA/checkpoint suites expanded; embeddings + recipes managers now covered with initialization, filtering, and duplicate workflows |
|
||||||
| Phase 4 | Interaction-level regression tests | Exercise template fragments, modals, and menus to ensure UI wiring remains intact | ✅ Complete | Vitest DOM suites cover NSFW selector, recipe modal editing, and global context menus |
|
| Phase 4 | Interaction-level regression tests | Exercise template fragments, modals, and menus to ensure UI wiring remains intact | ✅ Complete | Vitest DOM suites cover NSFW selector, recipe modal editing, and global context menus |
|
||||||
| Phase 5 | Continuous integration & coverage | Integrate frontend tests into CI workflow and track coverage metrics | ⚪ Not Started | Align reporting directories with backend coverage for unified reporting |
|
| Phase 5 | Continuous integration & coverage | Integrate frontend tests into CI workflow and track coverage metrics | ✅ Complete | CI workflow runs Vitest and aggregates V8 coverage into `coverage/frontend` via a dedicated script |
|
||||||
|
|
||||||
## Next Steps Checklist
|
## Next Steps Checklist
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ This roadmap tracks the planned rollout of automated testing for the ComfyUI LoR
|
|||||||
- [x] Add AppCore page feature suite exercising context menu creation and infinite scroll registration via DOM fixtures.
|
- [x] Add AppCore page feature suite exercising context menu creation and infinite scroll registration via DOM fixtures.
|
||||||
- [x] Extend AppCore orchestration tests to cover manager wiring, bulk menu setup, and onboarding gating scenarios.
|
- [x] Extend AppCore orchestration tests to cover manager wiring, bulk menu setup, and onboarding gating scenarios.
|
||||||
- [x] Add interaction regression suites for context menus and recipe modals to complete Phase 4.
|
- [x] Add interaction regression suites for context menus and recipe modals to complete Phase 4.
|
||||||
- [ ] Evaluate integrating coverage reporting once test surface grows (> 20 specs).
|
- [x] Evaluate integrating coverage reporting once test surface grows (> 20 specs).
|
||||||
- [x] Create shared fixtures for the loras and checkpoints pages once dedicated manager suites are added.
|
- [x] Create shared fixtures for the loras and checkpoints pages once dedicated manager suites are added.
|
||||||
- [x] Draft focused test matrix for loras/checkpoints manager filtering and sorting paths ahead of Phase 3.
|
- [x] Draft focused test matrix for loras/checkpoints manager filtering and sorting paths ahead of Phase 3.
|
||||||
- [x] Implement LoRAs manager filtering/sorting specs for scenarios F-01–F-05 & F-09; queue remaining edge cases after duplicate/bulk flows stabilize.
|
- [x] Implement LoRAs manager filtering/sorting specs for scenarios F-01–F-05 & F-09; queue remaining edge cases after duplicate/bulk flows stabilize.
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest"
|
"test:watch": "vitest",
|
||||||
|
"test:coverage": "node scripts/run_frontend_coverage.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
|
|||||||
198
scripts/run_frontend_coverage.js
Executable file
198
scripts/run_frontend_coverage.js
Executable file
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { mkdirSync, rmSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = path.resolve(__dirname, '..');
|
||||||
|
const coverageRoot = path.join(repoRoot, 'coverage');
|
||||||
|
const v8OutputDir = path.join(coverageRoot, '.v8');
|
||||||
|
const frontendCoverageDir = path.join(coverageRoot, 'frontend');
|
||||||
|
|
||||||
|
rmSync(v8OutputDir, { recursive: true, force: true });
|
||||||
|
rmSync(frontendCoverageDir, { recursive: true, force: true });
|
||||||
|
mkdirSync(v8OutputDir, { recursive: true });
|
||||||
|
mkdirSync(frontendCoverageDir, { recursive: true });
|
||||||
|
|
||||||
|
const vitestBinName = process.platform === 'win32' ? 'vitest.cmd' : 'vitest';
|
||||||
|
const vitestBin = path.join(repoRoot, 'node_modules', '.bin', vitestBinName);
|
||||||
|
|
||||||
|
const env = { ...process.env, NODE_V8_COVERAGE: v8OutputDir };
|
||||||
|
const result = spawnSync(vitestBin, ['run'], { stdio: 'inherit', env });
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.error('Failed to execute Vitest:', result.error.message);
|
||||||
|
process.exit(result.status ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
process.exit(result.status ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileCoverage = collectCoverageFromV8(v8OutputDir, repoRoot);
|
||||||
|
writeCoverageOutputs(fileCoverage, frontendCoverageDir, repoRoot);
|
||||||
|
printSummary(fileCoverage);
|
||||||
|
rmSync(v8OutputDir, { recursive: true, force: true });
|
||||||
|
|
||||||
|
function collectCoverageFromV8(v8Dir, rootDir) {
|
||||||
|
const coverageMap = new Map();
|
||||||
|
const files = readdirSync(v8Dir).filter((file) => file.endsWith('.json'));
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const reportPath = path.join(v8Dir, file);
|
||||||
|
const report = JSON.parse(readFileSync(reportPath, 'utf8'));
|
||||||
|
if (!Array.isArray(report.result)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const script of report.result) {
|
||||||
|
const filePath = normalizeFilePath(script.url, rootDir);
|
||||||
|
if (!filePath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filePath.startsWith('static/')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filePath.endsWith('.js')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePath = path.join(rootDir, filePath);
|
||||||
|
let lineMap = coverageMap.get(filePath);
|
||||||
|
if (!lineMap) {
|
||||||
|
lineMap = new Map();
|
||||||
|
coverageMap.set(filePath, lineMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = readFileSync(absolutePath, 'utf8');
|
||||||
|
const lineOffsets = calculateLineOffsets(source);
|
||||||
|
|
||||||
|
for (const fn of script.functions ?? []) {
|
||||||
|
for (const range of fn.ranges ?? []) {
|
||||||
|
if (range.startOffset === range.endOffset) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const count = typeof range.count === 'number' ? range.count : 0;
|
||||||
|
const startLine = findLineNumber(range.startOffset, lineOffsets);
|
||||||
|
const endLine = findLineNumber(Math.max(range.endOffset - 1, range.startOffset), lineOffsets);
|
||||||
|
for (let line = startLine; line <= endLine; line += 1) {
|
||||||
|
const current = lineMap.get(line);
|
||||||
|
if (current === undefined || count > current) {
|
||||||
|
lineMap.set(line, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return coverageMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeFilePath(url, rootDir) {
|
||||||
|
if (!url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
if (parsed.protocol !== 'file:') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const absolute = fileURLToPath(parsed);
|
||||||
|
const relative = path.relative(rootDir, absolute);
|
||||||
|
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return relative.replace(/\\/g, '/');
|
||||||
|
} catch {
|
||||||
|
if (url.startsWith(rootDir)) {
|
||||||
|
return url.slice(rootDir.length + 1).replace(/\\/g, '/');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateLineOffsets(content) {
|
||||||
|
const offsets = [0];
|
||||||
|
for (let index = 0; index < content.length; index += 1) {
|
||||||
|
if (content.charCodeAt(index) === 10) {
|
||||||
|
offsets.push(index + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offsets.push(content.length);
|
||||||
|
return offsets;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLineNumber(offset, lineOffsets) {
|
||||||
|
let low = 0;
|
||||||
|
let high = lineOffsets.length - 1;
|
||||||
|
while (low < high) {
|
||||||
|
const mid = Math.floor((low + high + 1) / 2);
|
||||||
|
if (lineOffsets[mid] <= offset) {
|
||||||
|
low = mid;
|
||||||
|
} else {
|
||||||
|
high = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return low + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeCoverageOutputs(coverageMap, outputDir, rootDir) {
|
||||||
|
const summary = {
|
||||||
|
total: { lines: { total: 0, covered: 0, pct: 100 } },
|
||||||
|
files: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
let lcovContent = '';
|
||||||
|
|
||||||
|
for (const [relativePath, lineMap] of [...coverageMap.entries()].sort()) {
|
||||||
|
const lines = [...lineMap.entries()].sort((a, b) => a[0] - b[0]);
|
||||||
|
const total = lines.length;
|
||||||
|
const covered = lines.filter(([, count]) => count > 0).length;
|
||||||
|
const pct = total === 0 ? 100 : (covered / total) * 100;
|
||||||
|
|
||||||
|
summary.files[relativePath] = {
|
||||||
|
lines: {
|
||||||
|
total,
|
||||||
|
covered,
|
||||||
|
pct,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
summary.total.lines.total += total;
|
||||||
|
summary.total.lines.covered += covered;
|
||||||
|
|
||||||
|
const absolute = path.join(rootDir, relativePath);
|
||||||
|
lcovContent += 'TN:\n';
|
||||||
|
lcovContent += `SF:${absolute.replace(/\\/g, '/')}\n`;
|
||||||
|
for (const [line, count] of lines) {
|
||||||
|
lcovContent += `DA:${line},${count}\n`;
|
||||||
|
}
|
||||||
|
lcovContent += `LF:${total}\n`;
|
||||||
|
lcovContent += `LH:${covered}\n`;
|
||||||
|
lcovContent += 'end_of_record\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.total.lines.pct = summary.total.lines.total === 0
|
||||||
|
? 100
|
||||||
|
: (summary.total.lines.covered / summary.total.lines.total) * 100;
|
||||||
|
|
||||||
|
writeFileSync(path.join(outputDir, 'coverage-summary.json'), JSON.stringify(summary, null, 2));
|
||||||
|
writeFileSync(path.join(outputDir, 'lcov.info'), lcovContent, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function printSummary(coverageMap) {
|
||||||
|
let totalLines = 0;
|
||||||
|
let totalCovered = 0;
|
||||||
|
for (const lineMap of coverageMap.values()) {
|
||||||
|
const lines = lineMap.size;
|
||||||
|
const covered = [...lineMap.values()].filter((count) => count > 0).length;
|
||||||
|
totalLines += lines;
|
||||||
|
totalCovered += covered;
|
||||||
|
}
|
||||||
|
const pct = totalLines === 0 ? 100 : (totalCovered / totalLines) * 100;
|
||||||
|
console.log(`\nFrontend coverage: ${totalCovered}/${totalLines} lines (${pct.toFixed(2)}%)`);
|
||||||
|
}
|
||||||
@@ -9,7 +9,9 @@ export default defineConfig({
|
|||||||
'tests/frontend/**/*.test.js'
|
'tests/frontend/**/*.test.js'
|
||||||
],
|
],
|
||||||
coverage: {
|
coverage: {
|
||||||
enabled: false,
|
enabled: process.env.VITEST_COVERAGE === 'true',
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'lcov', 'json-summary'],
|
||||||
reportsDirectory: 'coverage/frontend'
|
reportsDirectory: 'coverage/frontend'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user