Keep research search recoverable and isolate browser checks [skip ci]
This commit is contained in:
@@ -25,6 +25,11 @@ test("sample index has working pagination and returns to the same list", async (
|
||||
await expect(page.locator(".paper-row h3").first()).not.toHaveText(first);
|
||||
await page.getByRole("link", { name: "上一页" }).click();
|
||||
await expect(page.locator(".paper-row h3").first()).toHaveText(first);
|
||||
await page.goto("/?page=999999");
|
||||
await expect(page).toHaveURL(/page=3/);
|
||||
await expect(page.locator(".paper-row")).toHaveCount(4);
|
||||
await expect(page.getByRole("navigation", { name: "论文分页" })).toContainText("3 / 3");
|
||||
await expect(page.getByRole("link", { name: "下一页" })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("search, empty results, author scope and category filters work", async ({ page }) => {
|
||||
@@ -34,7 +39,7 @@ test("search, empty results, author scope and category filters work", async ({ p
|
||||
await expect(page.getByRole("heading", { name: "没有找到符合条件的论文" })).toBeVisible();
|
||||
await page.getByRole("link", { name: "查看全部论文" }).click();
|
||||
const author = snapshot.papers[0].authors[0];
|
||||
await page.getByLabel("搜索范围").selectOption("author");
|
||||
await page.getByRole("combobox", { name: "搜索范围", exact: true }).selectOption("author");
|
||||
await page.getByRole("textbox", { name: "搜索论文", exact: true }).fill(author);
|
||||
await page.getByRole("button", { name: "搜索论文", exact: true }).click();
|
||||
await expect(page.locator(".paper-row").first()).toContainText(author);
|
||||
@@ -45,7 +50,7 @@ test("search, empty results, author scope and category filters work", async ({ p
|
||||
for (const row of await page.locator(".paper-meta").all()) await expect(row).toContainText(topic);
|
||||
});
|
||||
|
||||
test("article preserves original abstract and offers citation copying", async ({ page }, info) => {
|
||||
test("article preserves original abstract and offers citation copying", async ({ page, baseURL }, info) => {
|
||||
const paper = snapshot.papers[0];
|
||||
await page.goto(`/papers/${paper.id}`);
|
||||
await expect(page.getByRole("heading", { level: 1 })).toHaveText(paper.title);
|
||||
@@ -57,7 +62,7 @@ test("article preserves original abstract and offers citation copying", async ({
|
||||
await expect(page.getByRole("link", { name: "登录", exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: info.outputPath("article.png"), fullPage: true });
|
||||
await page.getByRole("link", { name: "返回论文索引" }).click();
|
||||
await expect(page).toHaveURL("http://localhost:17322/");
|
||||
await expect(page).toHaveURL(new URL("/", baseURL).href);
|
||||
});
|
||||
|
||||
test("all reading themes apply and persist through a reload", async ({ page }) => {
|
||||
|
||||
Vendored
+18
-2
@@ -2,7 +2,7 @@
|
||||
// in a disposable Next dev copy; no real accounts, .env files or OAuth services.
|
||||
import { createServer } from "node:http";
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import { cp, mkdtemp, rm, symlink } from "node:fs/promises";
|
||||
import { cp, mkdtemp, readFile, rm, symlink } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -10,11 +10,14 @@ import { spawn } from "node:child_process";
|
||||
|
||||
const root = fileURLToPath(new URL("../../", import.meta.url));
|
||||
const origin = "http://localhost:17323";
|
||||
const snapshot = JSON.parse(await readFile(join(root, "src/data/papers.json"), "utf8"));
|
||||
const sessions = new Set(), grants = new Map();
|
||||
let meFailure = false, logoutFailure = false, verified = false;
|
||||
let counts = {};
|
||||
let paperStatus = 0, paperMalformed = false;
|
||||
function reset() {
|
||||
sessions.clear(); grants.clear(); meFailure = false; logoutFailure = false; verified = false;
|
||||
paperStatus = 0; paperMalformed = false;
|
||||
counts = { exchange: 0, verified: 0, logout: 0, google: 0, slack: 0 };
|
||||
}
|
||||
reset();
|
||||
@@ -42,8 +45,21 @@ const server = createServer(async (req, res) => {
|
||||
if (data.expire) sessions.clear();
|
||||
if (typeof data.meFailure === "boolean") meFailure = data.meFailure;
|
||||
if (typeof data.logoutFailure === "boolean") logoutFailure = data.logoutFailure;
|
||||
if ([0, 400, 429, 503, 504].includes(data.paperStatus)) paperStatus = data.paperStatus;
|
||||
if (typeof data.paperMalformed === "boolean") paperMalformed = data.paperMalformed;
|
||||
return reply(res, 200, {});
|
||||
}
|
||||
if (url.pathname.startsWith("/projects/thesisly/research/papers/")) {
|
||||
const paper = snapshot.papers.find(p => p.id === url.pathname.split("/").at(-1));
|
||||
return reply(res, paper ? 200 : 404, paper || { error: "not_found" });
|
||||
}
|
||||
if (url.pathname === "/projects/thesisly/research/papers") {
|
||||
if (paperStatus) return reply(res, paperStatus, { error: "synthetic research failure" });
|
||||
if (paperMalformed) return reply(res, 200, { unexpected: true });
|
||||
const page = Number(url.searchParams.get("page") || 1), pageSize = Number(url.searchParams.get("pageSize") || 8);
|
||||
// Fixture transport data, not an implementation of Core search semantics.
|
||||
return reply(res, 200, { items: snapshot.papers.slice((page - 1) * pageSize, page * pageSize), total: snapshot.papers.length, page, pageSize, topics: [...new Set(snapshot.papers.flatMap(p => p.topics))] });
|
||||
}
|
||||
const match = /^\/projects\/thesisly\/auth\/([a-z-]+)$/.exec(url.pathname);
|
||||
if (!match) return reply(res, 404, { error: "not_found" });
|
||||
const action = match[1];
|
||||
@@ -111,7 +127,7 @@ try {
|
||||
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(17324, "localhost", resolve); });
|
||||
const require = createRequire(import.meta.url);
|
||||
child = spawn(process.execPath, [require.resolve("next/dist/bin/next"), "dev", "--hostname", "localhost", "--port", "17323"], {
|
||||
cwd: directory, stdio: "inherit", env: { ...process.env, FOUNDRY_PROJECT: "thesisly", FOUNDRY_API_ORIGIN: "http://localhost:17324", APP_ORIGIN: origin, PAPER_API_ORIGIN: "", NEXT_TELEMETRY_DISABLED: "1" },
|
||||
cwd: directory, stdio: "inherit", env: { ...process.env, FOUNDRY_PROJECT: "thesisly", FOUNDRY_API_ORIGIN: "http://localhost:17324", APP_ORIGIN: origin, PAPER_API_ORIGIN: process.env.THESISLY_PAPER_MODE === "sample" ? "" : "http://localhost:17324/projects/thesisly/research/", NEXT_TELEMETRY_DISABLED: "1" },
|
||||
});
|
||||
child.once("exit", code => { if (!closing) void close(code || 1); });
|
||||
} catch (error) { console.error(error.message); await close(1); }
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createPaperTransport } from "../src/lib/paper-api.mjs";
|
||||
const origin = "https://core.example.test/projects/thesisly/research/";
|
||||
|
||||
test("paper transport uses the anonymous no-store API without forwarding site credentials", async () => {
|
||||
const transport = createPaperTransport(origin, async (url, options) => {
|
||||
assert.equal(url.href, origin + "papers?q=graph&page=2");
|
||||
assert.equal(options.cache, "no-store");
|
||||
assert.equal(options.redirect, "error");
|
||||
assert.equal(options.headers, undefined);
|
||||
assert.ok(options.signal instanceof AbortSignal);
|
||||
return Response.json({ items: [], total: 0, topics: [], page: 2, pageSize: 8 });
|
||||
});
|
||||
assert.deepEqual(await transport("papers?q=graph&page=2"), { ok: true, data: { items: [], total: 0, topics: [], page: 2, pageSize: 8 } });
|
||||
});
|
||||
|
||||
test("HTTP errors stay distinguishable and never become empty search results", async () => {
|
||||
for (const status of [400, 404, 429, 503, 504]) {
|
||||
const transport = createPaperTransport(origin, async () => Response.json({ error: "private upstream detail" }, { status }));
|
||||
assert.deepEqual(await transport("papers?q=graph"), { ok: false, status });
|
||||
}
|
||||
});
|
||||
|
||||
test("network, timeout and malformed responses are recoverable without sample substitution", async () => {
|
||||
for (const [error, status] of [[new TypeError("private connection detail"), 503], [new DOMException("deadline", "TimeoutError"), 504], [new DOMException("aborted", "AbortError"), 504]]) {
|
||||
assert.deepEqual(await createPaperTransport(origin, async () => { throw error; })("papers"), { ok: false, status });
|
||||
}
|
||||
assert.deepEqual(await createPaperTransport(origin, async () => new Response("invalid JSON"))("papers"), { ok: false, status: 502 });
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
const core = "http://localhost:17324";
|
||||
const errors = new WeakMap();
|
||||
async function control(request, data) { expect((await request.post(core + "/__test/control", { data })).ok()).toBe(true); }
|
||||
|
||||
test.beforeEach(async ({ page, request }) => {
|
||||
expect((await request.post(core + "/__test/reset")).ok()).toBe(true);
|
||||
errors.set(page, []);
|
||||
page.on("pageerror", error => errors.get(page).push(error.message));
|
||||
});
|
||||
test.afterEach(async ({ page }) => {
|
||||
expect(errors.get(page)).toEqual([]);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
||||
});
|
||||
|
||||
test("search failures retain controls and recover after a revised query", async ({ page, request }) => {
|
||||
for (const [status, heading] of [[504, "这次检索耗时较长"], [429, "论文索引正在处理较多请求"], [503, "论文暂时无法读取"], [400, "请调整搜索条件"]]) {
|
||||
await control(request, { paperStatus: status });
|
||||
await page.goto("/?q=original&topic=cs.LG&searchIn=author");
|
||||
await expect(page.getByRole("heading", { name: heading, exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("textbox", { name: "搜索论文", exact: true })).toHaveValue("original");
|
||||
await expect(page.getByLabel("搜索范围")).toHaveValue("author");
|
||||
await expect(page.locator('.search-form input[name="topic"]')).toHaveValue("cs.LG");
|
||||
await expect(page.locator(".index-heading h2 span, .pagination, .sample-note")).toHaveCount(0);
|
||||
await expect(page.getByRole("heading", { name: "没有找到符合条件的论文" })).toHaveCount(0);
|
||||
await expect(page.getByRole("link", { name: "重新尝试", exact: true })).toHaveAttribute("href", /q=original/);
|
||||
}
|
||||
await control(request, { paperStatus: 0 });
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.getByRole("textbox", { name: "搜索论文", exact: true }).fill("revised query");
|
||||
await page.getByRole("button", { name: "搜索论文", exact: true }).click();
|
||||
await expect(page).toHaveURL(/q=revised\+query/);
|
||||
await expect(page.locator(".paper-row")).toHaveCount(8);
|
||||
await expect(page.locator(".paper-index [role=alert]")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("malformed research responses do not become empty results or sample mode", async ({ page, request }) => {
|
||||
await control(request, { paperMalformed: true });
|
||||
await page.goto("/?q=keep-this-query");
|
||||
await expect(page.getByRole("heading", { name: "论文暂时无法读取", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("textbox", { name: "搜索论文", exact: true })).toHaveValue("keep-this-query");
|
||||
await expect(page.locator(".paper-row, .sample-note, .index-heading h2 span, .pagination")).toHaveCount(0);
|
||||
});
|
||||
Reference in New Issue
Block a user