新增样例文章实时验收测试
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
const baseURL = process.env.GEO_SAMPLE_BASE_URL ?? "http://127.0.0.1:3000";
|
||||||
|
const timeout = Number(process.env.GEO_SAMPLE_TIMEOUT_MS ?? "600000") + 60_000;
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests/e2e",
|
||||||
|
testMatch: "sample-flow.spec.ts",
|
||||||
|
fullyParallel: false,
|
||||||
|
workers: 1,
|
||||||
|
timeout,
|
||||||
|
retries: 0,
|
||||||
|
reporter: [["list"]],
|
||||||
|
use: {
|
||||||
|
baseURL,
|
||||||
|
trace: "retain-on-failure",
|
||||||
|
screenshot: "only-on-failure",
|
||||||
|
video: "retain-on-failure",
|
||||||
|
actionTimeout: 30_000,
|
||||||
|
navigationTimeout: 60_000,
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import { mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
import { test } from "@playwright/test";
|
||||||
|
|
||||||
|
import { loadArticleSamples } from "./sample-flow/sample-loader";
|
||||||
|
import { runSamplePageFlow } from "./sample-flow/page-flow";
|
||||||
|
import { writeSampleResult } from "./sample-flow/reporting";
|
||||||
|
import type {
|
||||||
|
ArticleSample,
|
||||||
|
FailureCategory,
|
||||||
|
SampleResult,
|
||||||
|
} from "./sample-flow/types";
|
||||||
|
|
||||||
|
const repoRoot = process.cwd();
|
||||||
|
const samplesDir =
|
||||||
|
process.env.GEO_SAMPLE_DIR ?? join(repoRoot, "samples", "articles");
|
||||||
|
const reportDir =
|
||||||
|
process.env.GEO_SAMPLE_REPORT_DIR ??
|
||||||
|
join(repoRoot, "test-results", "geo-sample-flow", "manual");
|
||||||
|
const baseURL = process.env.GEO_SAMPLE_BASE_URL ?? "http://127.0.0.1:3000";
|
||||||
|
const apiAccessKey = process.env.API_ACCESS_KEY ?? "";
|
||||||
|
const timeoutMs = Number(process.env.GEO_SAMPLE_TIMEOUT_MS ?? "600000");
|
||||||
|
const filter = process.env.GEO_SAMPLE_FILTER;
|
||||||
|
const limit = Number(process.env.GEO_SAMPLE_LIMIT ?? "0");
|
||||||
|
|
||||||
|
const loaded = loadArticleSamples(samplesDir);
|
||||||
|
const selectedSamples = selectSamples(loaded.valid, filter, limit);
|
||||||
|
|
||||||
|
test.describe("GEO sample article live E2E flow", () => {
|
||||||
|
test.beforeAll(() => {
|
||||||
|
mkdirSync(reportDir, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(reportDir, "invalid-samples.json"),
|
||||||
|
`${JSON.stringify(loaded.invalid, null, 2)}\n`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const sample of selectedSamples) {
|
||||||
|
test(sample.name, async ({ page, request }) => {
|
||||||
|
test.setTimeout(timeoutMs + 60_000);
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const consoleLines: string[] = [];
|
||||||
|
const pageErrors: string[] = [];
|
||||||
|
|
||||||
|
page.on("console", (message) => {
|
||||||
|
if (message.type() === "error") {
|
||||||
|
consoleLines.push(`${message.type()}: ${message.text()}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
page.on("pageerror", (error) => {
|
||||||
|
pageErrors.push(error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
let result: SampleResult;
|
||||||
|
try {
|
||||||
|
result = await runSamplePageFlow({
|
||||||
|
page,
|
||||||
|
request,
|
||||||
|
sample,
|
||||||
|
baseURL,
|
||||||
|
apiAccessKey,
|
||||||
|
reportDir,
|
||||||
|
timeoutMs,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const failureScreenshot = join(
|
||||||
|
reportDir,
|
||||||
|
"samples",
|
||||||
|
sample.slug,
|
||||||
|
"failure.png",
|
||||||
|
);
|
||||||
|
await page
|
||||||
|
.screenshot({ path: failureScreenshot, fullPage: true })
|
||||||
|
.catch(() => {});
|
||||||
|
const failedResult = buildFailureResult({
|
||||||
|
sample,
|
||||||
|
error,
|
||||||
|
startedAt,
|
||||||
|
failureScreenshot,
|
||||||
|
consoleLines,
|
||||||
|
pageErrors,
|
||||||
|
});
|
||||||
|
writeSampleResult(reportDir, failedResult);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enriched = enrichWithBrowserErrors(result, consoleLines, pageErrors);
|
||||||
|
writeSampleResult(reportDir, enriched);
|
||||||
|
if (enriched.status === "failed") {
|
||||||
|
throw new Error(enriched.failure_message ?? enriched.failure_category);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectSamples(
|
||||||
|
samples: ArticleSample[],
|
||||||
|
sampleFilter: string | undefined,
|
||||||
|
sampleLimit: number,
|
||||||
|
) {
|
||||||
|
const filtered = sampleFilter
|
||||||
|
? samples.filter(
|
||||||
|
(sample) =>
|
||||||
|
sample.fileName.includes(sampleFilter) ||
|
||||||
|
sample.name.includes(sampleFilter) ||
|
||||||
|
sample.slug.includes(sampleFilter),
|
||||||
|
)
|
||||||
|
: samples;
|
||||||
|
|
||||||
|
return sampleLimit > 0 ? filtered.slice(0, sampleLimit) : filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enrichWithBrowserErrors(
|
||||||
|
result: SampleResult,
|
||||||
|
consoleLines: string[],
|
||||||
|
pageErrors: string[],
|
||||||
|
): SampleResult {
|
||||||
|
if (consoleLines.length === 0 && pageErrors.length === 0) return result;
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
status: "failed",
|
||||||
|
failure_category: "console_error",
|
||||||
|
failure_message: [...consoleLines, ...pageErrors].join("\n"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFailureResult({
|
||||||
|
sample,
|
||||||
|
error,
|
||||||
|
startedAt,
|
||||||
|
failureScreenshot,
|
||||||
|
consoleLines,
|
||||||
|
pageErrors,
|
||||||
|
}: {
|
||||||
|
sample: ArticleSample;
|
||||||
|
error: unknown;
|
||||||
|
startedAt: number;
|
||||||
|
failureScreenshot: string;
|
||||||
|
consoleLines: string[];
|
||||||
|
pageErrors: string[];
|
||||||
|
}): SampleResult {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
file: sample.filePath,
|
||||||
|
name: sample.name,
|
||||||
|
slug: sample.slug,
|
||||||
|
status: "failed",
|
||||||
|
duration_ms: Date.now() - startedAt,
|
||||||
|
qa_fail_rules: [],
|
||||||
|
qa_warn_rules: [],
|
||||||
|
expected_hard_failures: sample.expectedHardFailures,
|
||||||
|
expected_warnings: sample.expectedWarnings,
|
||||||
|
exports: {
|
||||||
|
"optimized.md": "failed",
|
||||||
|
"optimized.docx": "failed",
|
||||||
|
"qa_report.json": "failed",
|
||||||
|
},
|
||||||
|
llm_tasks: [],
|
||||||
|
failure_category: categorizeFailure(message),
|
||||||
|
failure_message: [...consoleLines, ...pageErrors, message]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n"),
|
||||||
|
artifacts: {
|
||||||
|
failure_screenshot: failureScreenshot,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function categorizeFailure(message: string): FailureCategory {
|
||||||
|
if (/timeout/i.test(message)) return "stream_timeout";
|
||||||
|
if (/LLM|provider|schema|quota|rate/i.test(message)) return "llm_failed";
|
||||||
|
if (/export|optimized\.md|optimized\.docx|qa_report\.json/i.test(message)) {
|
||||||
|
return "export_failed";
|
||||||
|
}
|
||||||
|
if (/console|pageerror/i.test(message)) return "console_error";
|
||||||
|
if (/failed|优化失败/i.test(message)) return "stream_failed";
|
||||||
|
return "page_flow_failed";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user