Commit d7af8fce authored by nanahira's avatar nanahira

filters

parent 30f1c126
......@@ -9,3 +9,33 @@
.progress {
height: 0.75rem;
}
.report-scroll {
max-height: 70vh;
overflow-y: auto;
padding-right: 0.5rem;
}
details > summary {
cursor: pointer;
list-style: none;
}
details > summary::-webkit-details-marker {
display: none;
}
.summary-row {
user-select: none;
}
details > summary::after {
content: "▾";
color: #6c757d;
margin-left: 0.5rem;
transition: transform 0.15s ease;
}
details[open] > summary::after {
transform: rotate(180deg);
}
......@@ -7,23 +7,35 @@
</p>
</header>
<section class="card shadow-sm mb-4">
<section class="card shadow-sm mb-4 no-print">
<div class="card-body">
<div class="mb-3">
<label class="form-label" for="packageInput">选择扩展包</label>
<input
id="packageInput"
class="form-control"
type="file"
accept=".zip,.ypk,application/zip"
multiple
[disabled]="running()"
(click)="onFileInputClick($event)"
(change)="onFileSelection($event)"
aria-describedby="packageHelp"
/>
<div id="packageHelp" class="form-text">
支持多选,支持 zip 或 ypk。
<div class="d-flex align-items-center gap-2">
<div class="flex-grow-1">
<label class="form-label" for="packageInput">选择扩展包</label>
<input
id="packageInput"
class="form-control"
type="file"
accept=".zip,.ypk,application/zip"
multiple
[disabled]="running()"
(click)="onFileInputClick($event)"
(change)="onFileSelection($event)"
aria-describedby="packageHelp"
/>
<div id="packageHelp" class="form-text">
支持多选,支持 zip 或 ypk。
</div>
</div>
<button
type="button"
class="btn btn-outline-secondary btn-sm"
[disabled]="running()"
(click)="clearSelection()"
>
清空
</button>
</div>
</div>
......@@ -34,20 +46,15 @@
@for (file of selectedFiles(); track file.name) {
<li class="list-group-item px-0 d-flex justify-content-between">
<span>{{ file.name }}</span>
<span class="text-muted small">{{ file.size }} bytes</span>
<span class="text-muted small">{{ formatFileSize(file.size) }}</span>
</li>
}
</ul>
</div>
}
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn-primary" [disabled]="!canStart()" (click)="startTests()">
开始测试
</button>
<button type="button" class="btn btn-outline-secondary" [disabled]="running()" (click)="clearSelection()">
清空
</button>
<div class="small text-muted">
选择文件后将自动开始测试。
</div>
</div>
</section>
......@@ -58,7 +65,7 @@
</div>
}
<section class="card shadow-sm mb-4" [attr.aria-busy]="running()">
<section class="card shadow-sm mb-4 no-print" [attr.aria-busy]="running()">
<div class="card-body">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-2">
<div>
......@@ -96,44 +103,124 @@
@if (hasResults()) {
<section class="mb-5" aria-live="polite">
<h2 class="h5 mb-3">测试报告</h2>
@for (report of reports(); track report.fileName) {
<article class="card shadow-sm mb-4">
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
<div>
<h3 class="h6 mb-1">{{ report.fileName }}</h3>
<div class="small text-muted">{{ packageCardSummary(report) }}</div>
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-2">
<h2 class="h5 mb-0">测试报告</h2>
<div class="d-flex flex-wrap align-items-center gap-2 no-print">
@if (zipFilterOptions().length > 1) {
<label class="form-label small mb-0" for="zipFilter">扩展包筛选</label>
<select
id="zipFilter"
class="form-select form-select-sm w-auto"
[value]="zipFilter()"
(change)="onZipFilterChange($event)"
>
<option value="all">全部</option>
@if (zipFilterOptions().includes('passed')) {
<option value="passed">正常</option>
}
@if (zipFilterOptions().includes('failed')) {
<option value="failed">异常</option>
}
@if (zipFilterOptions().includes('empty')) {
<option value="empty">无内容</option>
}
</select>
}
<button type="button" class="btn btn-outline-secondary btn-sm" (click)="exportPdf()">
导出 PDF
</button>
</div>
</div>
<div class="card shadow-sm mb-3">
<div class="card-body py-2">
<div class="row g-2 small">
<div class="col-md-4">
扩展包:{{ overallStats().zipTotal }},正常 {{ overallStats().zipPassed }},异常
{{ overallStats().zipFailed }}
</div>
<div class="col-md-4">
CDB:{{ overallStats().cdbTotal }},正常 {{ overallStats().cdbPassed }},异常
{{ overallStats().cdbFailed }}
</div>
<div class="col-md-4">
卡片:{{ overallStats().cardTotal }},通过 {{ overallStats().cardPassed }},失败
{{ overallStats().cardFailed }}
</div>
<span [class]="packageBadgeClass(report.status)">
{{ packageStatusLabel(report.status) }}
</span>
</div>
<div class="card-body">
</div>
</div>
<div class="report-scroll" tabindex="0" aria-label="测试报告滚动区域">
@if (visibleReports().length === 0) {
<div class="text-muted">当前筛选无扩展包。</div>
}
@for (report of visibleReports(); track report.fileName) {
<details class="card shadow-sm mb-4" open>
<summary class="card-header summary-row d-flex flex-wrap align-items-center gap-2">
<div>
<h3 class="h6 mb-1">{{ report.fileName }}</h3>
<div class="small text-muted">
{{ packageCardSummary(report) }} · {{ packageCdbSummary(report) }}
</div>
</div>
<span class="ms-auto" [class]="packageBadgeClass(report)">
{{ packageStatusLabel(report) }}
</span>
</summary>
<div class="card-body">
@if (report.error) {
<div class="alert alert-danger" role="alert">
{{ report.error }}
</div>
}
@if (cdbFilterOptions(report).length > 1) {
<div class="d-flex flex-wrap align-items-center gap-2 mb-3">
<label class="form-label small mb-0" for="cdbFilter-{{ packageIndexOf(report) }}">
CDB 筛选
</label>
<select
id="cdbFilter-{{ packageIndexOf(report) }}"
class="form-select form-select-sm w-auto"
[value]="report.cdbFilter"
(change)="onPackageFilterChange($event, report)"
>
<option value="all">全部</option>
@if (cdbFilterOptions(report).includes('passed')) {
<option value="passed">正常</option>
}
@if (cdbFilterOptions(report).includes('failed')) {
<option value="failed">异常</option>
}
@if (cdbFilterOptions(report).includes('empty')) {
<option value="empty">无内容</option>
}
</select>
</div>
}
@if (report.cdbs.length === 0) {
<div class="text-muted">未在根目录找到 cdb 文件。</div>
} @else if (visibleCdbs(report).length === 0) {
<div class="text-muted">当前筛选无 cdb。</div>
}
@for (cdb of report.cdbs; track cdb.fileName) {
<section class="border rounded p-3 mb-3">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-2">
@for (cdb of visibleCdbs(report); track cdb.fileName) {
<details class="border rounded p-3 mb-3" open>
<summary class="summary-row d-flex flex-wrap align-items-start gap-2">
<div>
<h4 class="h6 mb-1">{{ cdb.fileName }}</h4>
<div class="small text-muted">
卡片 {{ cdb.totalCards }},已测 {{ cdb.testedCards }},通过 {{ cdb.passedCards }},失败
卡片 {{ cdb.totalTestableCards }},已测 {{ cdb.testedCards }},通过 {{ cdb.passedCards }},失败
{{ cdb.failedCards }}
</div>
</div>
<span [class]="cdbBadgeClass(cdb.status)">
{{ cdbStatusLabel(cdb.status) }}
<span class="ms-auto" [class]="cdbBadgeClass(cdb)">
{{ cdbStatusLabel(cdb) }}
</span>
</div>
</summary>
@if (cdb.error) {
<div class="alert alert-danger mt-2" role="alert">
......@@ -141,11 +228,44 @@
</div>
}
@if (cardFilterOptions(cdb).length > 1) {
<div class="d-flex flex-wrap align-items-center gap-2 mt-2">
<label
class="form-label small mb-0"
for="cardFilter-{{ packageIndexOf(report) }}-{{ cdbIndexOf(report, cdb) }}"
>
卡片筛选
</label>
<select
id="cardFilter-{{ packageIndexOf(report) }}-{{ cdbIndexOf(report, cdb) }}"
class="form-select form-select-sm w-auto"
[value]="cdb.cardFilter"
(change)="onCdbFilterChange($event, report, cdb)"
>
<option value="all">全部</option>
@if (cardFilterOptions(cdb).includes('passed')) {
<option value="passed">通过</option>
}
@if (cardFilterOptions(cdb).includes('failed')) {
<option value="failed">失败</option>
}
@if (cardFilterOptions(cdb).includes('skip')) {
<option value="skip">无需测试</option>
}
@if (cardFilterOptions(cdb).includes('empty')) {
<option value="empty">无内容</option>
}
</select>
</div>
}
@if (cdb.cards.length === 0) {
<div class="text-muted mt-2">没有需要测试的卡片。</div>
} @else if (visibleCards(cdb).length === 0) {
<div class="text-muted mt-2">当前筛选无卡片。</div>
}
@for (card of cdb.cards; track card.id) {
@for (card of visibleCards(cdb); track card.id) {
<div class="card mt-3">
<div class="card-body py-2">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-2">
......@@ -154,12 +274,22 @@
{{ card.name }}
<span class="text-muted">#{{ card.id }}</span>
</div>
<div class="small text-muted">
ScriptError 数量:{{ card.scriptErrors.length }}
</div>
@if (card.skipTest) {
<div class="small text-muted">无需测试</div>
} @else if (card.scriptErrors.length > 0) {
<div class="small text-muted">
错误信息数量:{{ card.scriptErrors.length }}
</div>
} @else if (card.status === 'passed') {
<div class="small text-muted">卡片状态正常</div>
} @else if (card.status === 'pending') {
<div class="small text-muted">等待测试输出。</div>
} @else if (card.status === 'running') {
<div class="small text-muted">正在生成测试输出...</div>
}
</div>
<span [class]="cardBadgeClass(card.status)">
{{ cardStatusLabel(card.status) }}
<span [class]="cardBadgeClassForCard(card)">
{{ cardStatusLabelForCard(card) }}
</span>
</div>
......@@ -170,13 +300,9 @@
</div>
}
<div class="mt-2">
<div class="small text-muted">测试输出</div>
@if (card.status === 'pending') {
<div class="small text-muted">等待测试输出。</div>
} @else if (card.status === 'running') {
<div class="small text-muted">正在生成测试输出...</div>
} @else if (card.logs.length > 0) {
@if (card.logs.length > 0) {
<div class="mt-2">
<div class="small text-muted">测试输出</div>
<ul class="small mb-0">
@for (entry of card.logs; track $index) {
<li
......@@ -187,23 +313,22 @@
</li>
}
</ul>
} @else if (card.status === 'error') {
<div class="small text-muted">测试未生成日志输出。</div>
} @else {
<div class="small text-muted">卡片状态正常</div>
}
</div>
</div>
}
</div>
</div>
}
</section>
</details>
}
</div>
</article>
}
</div>
</details>
}
</div>
</section>
} @else {
<div class="text-muted">暂无测试报告。</div>
}
</div>
<footer class="py-4 text-center text-muted small">
<span>© 2026 Nanahira</span>
</footer>
</main>
......@@ -20,8 +20,16 @@ import {
import { YGOPRO_CDB_URL, YGOPRO_SCRIPT_ZIP_URL } from './resource-urls';
type PackageStatus = 'pending' | 'running' | 'done' | 'error';
type CdbStatus = 'pending' | 'running' | 'done' | 'skipped' | 'error';
type CdbStatus = 'pending' | 'running' | 'done' | 'error';
type CardStatus = 'pending' | 'running' | 'passed' | 'failed' | 'error';
type FilterOption = 'all' | 'passed' | 'failed' | 'skip' | 'empty';
type DisplayStatus =
| 'pending'
| 'running'
| 'normal'
| 'abnormal'
| 'error'
| 'empty';
type CardTestResult = {
id: number;
......@@ -29,13 +37,16 @@ type CardTestResult = {
status: CardStatus;
scriptErrors: string[];
logs: TestCardMessage[];
skipTest: boolean;
detail?: string;
};
type CdbTestResult = {
fileName: string;
status: CdbStatus;
cardFilter: FilterOption;
cards: CardTestResult[];
totalTestableCards: number;
totalCards: number;
testedCards: number;
passedCards: number;
......@@ -46,6 +57,7 @@ type CdbTestResult = {
type PackageTestResult = {
fileName: string;
status: PackageStatus;
cdbFilter: FilterOption;
cdbs: CdbTestResult[];
error?: string;
};
......@@ -60,6 +72,7 @@ type ProgressSnapshot = {
type SqlCardRow = {
id: number;
name: string | null;
type: number | null;
};
type ZipCdbEntry = {
......@@ -93,7 +106,7 @@ export class App {
let failed = 0;
for (const report of this.reports()) {
for (const cdb of report.cdbs) {
total += cdb.totalCards;
total += cdb.totalTestableCards;
done += cdb.testedCards;
failed += cdb.failedCards;
}
......@@ -111,6 +124,61 @@ export class App {
});
protected readonly hasResults = computed(() => this.reports().length > 0);
protected readonly zipFilter = signal<FilterOption>('all');
protected readonly visibleReports = computed(() =>
this.reports().filter((report) =>
this.matchesFilter(
this.packageDisplayStatus(report),
this.resolveFilter(this.zipFilter(), this.zipFilterOptions()),
),
),
);
protected readonly overallStats = computed(() => {
const reports = this.reports();
const zipTotal = reports.length;
let zipPassed = 0;
let zipFailed = 0;
let cdbTotal = 0;
let cdbPassed = 0;
let cdbFailed = 0;
let cardTotal = 0;
let cardPassed = 0;
let cardFailed = 0;
for (const report of reports) {
const zipStatus = this.packageDisplayStatus(report);
if (zipStatus === 'normal') {
zipPassed += 1;
} else if (zipStatus === 'abnormal' || zipStatus === 'error') {
zipFailed += 1;
}
for (const cdb of report.cdbs) {
cdbTotal += 1;
const cdbStatus = this.cdbDisplayStatus(cdb);
if (cdbStatus === 'normal') {
cdbPassed += 1;
} else if (cdbStatus === 'abnormal' || cdbStatus === 'error') {
cdbFailed += 1;
}
cardTotal += cdb.totalTestableCards;
cardPassed += cdb.passedCards;
cardFailed += cdb.failedCards;
}
}
return {
zipTotal,
zipPassed,
zipFailed,
cdbTotal,
cdbPassed,
cdbFailed,
cardTotal,
cardPassed,
cardFailed,
};
});
private sqlModule: SqlJsStatic | null = null;
private baseDb: Database | null = null;
......@@ -130,10 +198,12 @@ export class App {
this.selectedFiles.set(files);
this.reports.set([]);
this.globalError.set(null);
this.zipFilter.set('all');
if (files.length === 0) {
this.statusMessage.set('等待上传扩展包。');
} else {
this.statusMessage.set(`已选择 ${files.length} 个扩展包,准备开始测试。`);
this.statusMessage.set(`已选择 ${files.length} 个扩展包,开始测试。`);
void this.startTests();
}
}
......@@ -141,6 +211,7 @@ export class App {
this.selectedFiles.set([]);
this.reports.set([]);
this.globalError.set(null);
this.zipFilter.set('all');
this.statusMessage.set('已清空选择。');
}
......@@ -156,6 +227,7 @@ export class App {
files.map((file) => ({
fileName: file.name,
status: 'pending',
cdbFilter: 'all',
cdbs: [],
})),
);
......@@ -184,12 +256,16 @@ export class App {
}
}
protected packageBadgeClass(status: PackageStatus): string {
switch (status) {
protected packageBadgeClass(report: PackageTestResult): string {
switch (this.packageDisplayStatus(report)) {
case 'running':
return 'badge text-bg-primary';
case 'done':
case 'normal':
return 'badge text-bg-success';
case 'abnormal':
return 'badge text-bg-warning';
case 'empty':
return 'badge text-bg-secondary';
case 'error':
return 'badge text-bg-danger';
default:
......@@ -197,12 +273,16 @@ export class App {
}
}
protected packageStatusLabel(status: PackageStatus): string {
switch (status) {
protected packageStatusLabel(report: PackageTestResult): string {
switch (this.packageDisplayStatus(report)) {
case 'running':
return '测试中';
case 'done':
return '已完成';
case 'normal':
return '正常';
case 'abnormal':
return '异常';
case 'empty':
return '无内容';
case 'error':
return '出错';
default:
......@@ -210,14 +290,16 @@ export class App {
}
}
protected cdbBadgeClass(status: CdbStatus): string {
switch (status) {
protected cdbBadgeClass(cdb: CdbTestResult): string {
switch (this.cdbDisplayStatus(cdb)) {
case 'running':
return 'badge text-bg-primary';
case 'done':
case 'normal':
return 'badge text-bg-success';
case 'skipped':
case 'abnormal':
return 'badge text-bg-warning';
case 'empty':
return 'badge text-bg-secondary';
case 'error':
return 'badge text-bg-danger';
default:
......@@ -225,14 +307,16 @@ export class App {
}
}
protected cdbStatusLabel(status: CdbStatus): string {
switch (status) {
protected cdbStatusLabel(cdb: CdbTestResult): string {
switch (this.cdbDisplayStatus(cdb)) {
case 'running':
return '测试中';
case 'done':
return '已完成';
case 'skipped':
return '跳过';
case 'normal':
return '正常';
case 'abnormal':
return '异常';
case 'empty':
return '无内容';
case 'error':
return '出错';
default:
......@@ -269,12 +353,134 @@ export class App {
}
}
protected cardBadgeClassForCard(card: CardTestResult): string {
if (card.skipTest) {
return 'badge text-bg-secondary';
}
return this.cardBadgeClass(card.status);
}
protected cardStatusLabelForCard(card: CardTestResult): string {
if (card.skipTest) {
return '无需测试';
}
return this.cardStatusLabel(card.status);
}
protected visibleCdbs(report: PackageTestResult): CdbTestResult[] {
const filter = this.resolveFilter(report.cdbFilter, this.cdbFilterOptions(report));
return report.cdbs.filter((cdb) =>
this.matchesFilter(this.cdbDisplayStatus(cdb), filter),
);
}
protected visibleCards(cdb: CdbTestResult): CardTestResult[] {
const filter = this.resolveFilter(cdb.cardFilter, this.cardFilterOptions(cdb));
return cdb.cards.filter((card) =>
this.matchesFilter(this.cardDisplayStatus(card), filter),
);
}
protected zipFilterOptions(): FilterOption[] {
return this.filterOptionsForStatuses(
this.reports().map((report) => this.packageDisplayStatus(report)),
);
}
protected cdbFilterOptions(report: PackageTestResult): FilterOption[] {
return this.filterOptionsForStatuses(
report.cdbs.map((cdb) => this.cdbDisplayStatus(cdb)),
);
}
protected cardFilterOptions(cdb: CdbTestResult): FilterOption[] {
return this.filterOptionsForStatuses(
cdb.cards.map((card) => this.cardDisplayStatus(card)),
);
}
protected onZipFilterChange(event: Event): void {
const target = event.target as HTMLSelectElement | null;
const value = target?.value ?? 'all';
if (!this.isFilterOption(value)) {
return;
}
this.zipFilter.set(value);
}
protected onPackageFilterChange(
event: Event,
report: PackageTestResult,
): void {
const target = event.target as HTMLSelectElement | null;
const value = target?.value ?? 'all';
if (!this.isFilterOption(value)) {
return;
}
const packageIndex = this.packageIndexOf(report);
if (packageIndex < 0) {
return;
}
this.updatePackage(packageIndex, (current) => ({
...current,
cdbFilter: value,
}));
}
protected onCdbFilterChange(
event: Event,
report: PackageTestResult,
cdb: CdbTestResult,
): void {
const target = event.target as HTMLSelectElement | null;
const value = target?.value ?? 'all';
if (!this.isFilterOption(value)) {
return;
}
const packageIndex = this.packageIndexOf(report);
if (packageIndex < 0) {
return;
}
const cdbIndex = this.cdbIndexOf(report, cdb);
if (cdbIndex < 0) {
return;
}
this.updateCdb(packageIndex, cdbIndex, (current) => ({
...current,
cardFilter: value,
}));
}
protected packageIndexOf(report: PackageTestResult): number {
return this.reports().indexOf(report);
}
protected cdbIndexOf(report: PackageTestResult, cdb: CdbTestResult): number {
return report.cdbs.indexOf(cdb);
}
protected formatFileSize(bytes: number): string {
if (bytes >= 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
if (bytes >= 1024) {
return `${Math.round(bytes / 1024)} KB`;
}
return `${bytes} bytes`;
}
protected exportPdf(): void {
if (typeof window !== 'undefined') {
window.print();
}
}
protected packageCardSummary(report: PackageTestResult): string {
let total = 0;
let passed = 0;
let failed = 0;
for (const cdb of report.cdbs) {
total += cdb.totalCards;
total += cdb.totalTestableCards;
passed += cdb.passedCards;
failed += cdb.failedCards;
}
......@@ -284,6 +490,23 @@ export class App {
return `卡片 ${total},通过 ${passed},失败 ${failed}`;
}
protected packageCdbSummary(report: PackageTestResult): string {
if (report.cdbs.length === 0) {
return 'CDB 0';
}
let passed = 0;
let failed = 0;
for (const cdb of report.cdbs) {
const status = this.cdbDisplayStatus(cdb);
if (status === 'normal') {
passed += 1;
} else if (status === 'abnormal' || status === 'error') {
failed += 1;
}
}
return `CDB ${report.cdbs.length},正常 ${passed},异常 ${failed}`;
}
private async testPackage(file: File, packageIndex: number): Promise<void> {
this.updatePackage(packageIndex, (report) => ({
...report,
......@@ -296,7 +519,9 @@ export class App {
const buffer = await file.arrayBuffer();
const zip = await JSZip.loadAsync(buffer);
const cdbEntries = this.extractRootCdbEntries(zip);
const cdbReports = cdbEntries.map((entry) => this.createCdbReport(entry.displayName));
const cdbReports = cdbEntries.map((entry) =>
this.createCdbReport(entry.displayName),
);
this.updatePackage(packageIndex, (report) => ({
...report,
......@@ -330,7 +555,8 @@ export class App {
}
const snapshot = this.reports()[packageIndex];
const hasError = snapshot?.cdbs.some((cdb) => cdb.status === 'error') ?? false;
const hasError =
snapshot?.cdbs.some((cdb) => cdb.status === 'error') ?? false;
this.updatePackage(packageIndex, (report) => ({
...report,
status: hasError ? 'error' : 'done',
......@@ -387,7 +613,8 @@ export class App {
if (cards.length === 0) {
this.updateCdb(packageIndex, cdbIndex, (cdb) => ({
...cdb,
status: 'skipped',
status: 'done',
totalTestableCards: 0,
totalCards: 0,
testedCards: 0,
passedCards: 0,
......@@ -403,12 +630,15 @@ export class App {
status: 'pending',
scriptErrors: [],
logs: [],
skipTest: card.skipTest,
}));
const testableCards = cardResults.filter((card) => !card.skipTest);
this.updateCdb(packageIndex, cdbIndex, (cdb) => ({
...cdb,
cards: cardResults,
totalCards: cardResults.length,
totalTestableCards: testableCards.length,
testedCards: 0,
passedCards: 0,
failedCards: 0,
......@@ -416,6 +646,9 @@ export class App {
for (let cardIndex = 0; cardIndex < cardResults.length; cardIndex += 1) {
const card = cardResults[cardIndex];
if (card.skipTest) {
continue;
}
this.statusMessage.set(`正在测试 ${entry.displayName} - ${card.name}`);
this.updateCard(packageIndex, cdbIndex, cardIndex, (item) => ({
...item,
......@@ -479,7 +712,9 @@ export class App {
return {
fileName,
status: 'pending',
cardFilter: 'all',
cards: [],
totalTestableCards: 0,
totalCards: 0,
testedCards: 0,
passedCards: 0,
......@@ -580,6 +815,7 @@ export class App {
status: scriptErrors.length === 0 ? 'passed' : 'failed',
scriptErrors,
logs,
skipTest: false,
};
} catch (error) {
return {
......@@ -588,6 +824,7 @@ export class App {
status: 'error',
scriptErrors: [],
logs: [],
skipTest: false,
detail: this.formatErrorStack(error),
};
} finally {
......@@ -595,25 +832,28 @@ export class App {
}
}
private queryTestCards(db: Database): Array<{ id: number; name: string }> {
private queryTestCards(db: Database): Array<{ id: number; name: string; skipTest: boolean }> {
const normalMonsterType =
(OcgcoreCommonConstants.TYPE_NORMAL | OcgcoreCommonConstants.TYPE_MONSTER) >>> 0;
const tokenType = OcgcoreCommonConstants.TYPE_TOKEN >>> 0;
const stmt = db.prepare(
'SELECT datas.id as id, texts.name as name FROM datas INNER JOIN texts ON datas.id = texts.id WHERE datas.type != ? AND (datas.type & ?) = 0',
'SELECT datas.id as id, texts.name as name, datas.type as type FROM datas INNER JOIN texts ON datas.id = texts.id',
);
try {
const result: Array<{ id: number; name: string }> = [];
stmt.bind([normalMonsterType, tokenType]);
const result: Array<{ id: number; name: string; skipTest: boolean }> = [];
while (stmt.step()) {
const row = stmt.getAsObject() as unknown as SqlCardRow;
if (!row || row.id == null) {
continue;
}
const typeValue = (row.type ?? 0) >>> 0;
const skipTest =
typeValue === normalMonsterType || (typeValue & tokenType) !== 0;
result.push({
id: row.id,
name: row.name ?? '未命名卡片',
skipTest,
});
}
return result;
......@@ -719,6 +959,146 @@ export class App {
return status === 'failed' || status === 'error';
}
private cardDisplayStatus(
card: CardTestResult,
): 'passed' | 'failed' | 'pending' | 'skip' {
if (card.skipTest) {
return 'skip';
}
if (card.status === 'passed') {
return 'passed';
}
if (card.status === 'failed' || card.status === 'error') {
return 'failed';
}
return 'pending';
}
private cdbDisplayStatus(cdb: CdbTestResult): DisplayStatus {
if (cdb.status === 'error') {
return 'error';
}
if (cdb.status === 'running') {
return 'running';
}
if (cdb.totalTestableCards === 0 && cdb.status === 'done') {
return 'empty';
}
if (cdb.failedCards > 0) {
return 'abnormal';
}
if (cdb.totalTestableCards > 0 && cdb.testedCards === cdb.totalTestableCards) {
return 'normal';
}
return 'pending';
}
private packageDisplayStatus(report: PackageTestResult): DisplayStatus {
if (report.status === 'error') {
return 'error';
}
if (report.status === 'running') {
return 'running';
}
if (report.status === 'done' && report.cdbs.length === 0) {
return 'empty';
}
if (
report.status === 'done' &&
report.cdbs.length > 0 &&
report.cdbs.every((cdb) => this.cdbDisplayStatus(cdb) === 'empty')
) {
return 'empty';
}
const hasFailed =
report.cdbs.some((cdb) => this.cdbDisplayStatus(cdb) === 'abnormal') ||
report.cdbs.some((cdb) => this.cdbDisplayStatus(cdb) === 'error');
if (hasFailed) {
return 'abnormal';
}
const allDone = report.cdbs.every((cdb) => {
const status = this.cdbDisplayStatus(cdb);
return status === 'normal' || status === 'empty' || status === 'error';
});
if (allDone && report.cdbs.length > 0) {
return 'normal';
}
return 'pending';
}
private matchesFilter(
status: 'passed' | 'failed' | DisplayStatus | 'skip',
filter: FilterOption,
): boolean {
if (filter === 'all') {
return true;
}
if (filter === 'passed') {
return status === 'passed' || status === 'normal';
}
if (filter === 'skip') {
return status === 'skip';
}
if (filter === 'empty') {
return status === 'empty';
}
return status === 'failed' || status === 'abnormal' || status === 'error';
}
private filterOptionsForStatuses(
statuses: Array<'passed' | 'failed' | DisplayStatus | 'pending' | 'skip'>,
): FilterOption[] {
let hasPassed = false;
let hasFailed = false;
let hasSkip = false;
let hasEmpty = false;
for (const status of statuses) {
if (status === 'passed' || status === 'normal') {
hasPassed = true;
} else if (status === 'failed' || status === 'abnormal' || status === 'error') {
hasFailed = true;
} else if (status === 'skip') {
hasSkip = true;
} else if (status === 'empty') {
hasEmpty = true;
}
}
const options: FilterOption[] = [];
if (hasPassed) {
options.push('passed');
}
if (hasFailed) {
options.push('failed');
}
if (hasSkip) {
options.push('skip');
}
if (hasEmpty) {
options.push('empty');
}
return options;
}
private resolveFilter(
current: FilterOption,
options: FilterOption[],
): FilterOption {
if (options.includes(current)) {
return current;
}
return 'all';
}
private isFilterOption(value: string): value is FilterOption {
return (
value === 'all' ||
value === 'passed' ||
value === 'failed' ||
value === 'skip' ||
value === 'empty'
);
}
private formatError(error: unknown): string {
if (error instanceof Error) {
return error.message;
......
......@@ -11,3 +11,19 @@ body {
main {
min-height: 100vh;
}
@media print {
body {
background: #ffffff;
}
.no-print {
display: none !important;
}
.report-scroll {
max-height: none !important;
overflow: visible !important;
padding-right: 0 !important;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment