merge: integrate WP5-05 and WP5-06 baseline
# Conflicts: # package.json
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { createServer, type ViteDevServer } from "vite";
|
||||
|
||||
const releaseVersion = "asset-20260803.1";
|
||||
const publicId = "7f0c9530-a7d9-4bf1-8c65-0e9298dd04ac";
|
||||
const previewId = "ab18fd72-60e1-44e3-a9a0-3dfccb12e17c";
|
||||
const privateId = "e3792605-5252-4d3b-a101-827408ab3515";
|
||||
let vite: ViteDevServer;
|
||||
let webUrl: string;
|
||||
const requestCounts = { preview: 0, private: 0, public: 0 };
|
||||
|
||||
function writeEvidence(directory: string | undefined, name: string, value: unknown) {
|
||||
if (!directory) return;
|
||||
mkdirSync(directory, { recursive: true });
|
||||
writeFileSync(resolve(directory, name), `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
test.beforeAll(async () => {
|
||||
vite = await createServer({
|
||||
configFile: false,
|
||||
plugins: [{
|
||||
name: "wp5-04-three-resource-classes",
|
||||
configureServer(server) {
|
||||
server.middlewares.use((request, response, next) => {
|
||||
const routes = [
|
||||
{ access: "public", body: "public-content", id: publicId, prefix: "/api/v1/assets/public/" },
|
||||
{ access: "preview", body: "preview-content", id: previewId, prefix: "/api/v1/assets/preview/" },
|
||||
{ access: "private", body: "private-content", id: privateId, prefix: "/api/v1/private-assets/" },
|
||||
] as const;
|
||||
const route = routes.find((item) => request.url === `${item.prefix}${releaseVersion}/${item.id}`);
|
||||
if (!route) return next();
|
||||
requestCounts[route.access] += 1;
|
||||
response.statusCode = 200;
|
||||
response.setHeader("Cache-Control", route.access === "public" ? "public, max-age=31536000, immutable" : "private, no-store");
|
||||
response.setHeader("Content-Type", "application/octet-stream");
|
||||
response.end(route.body);
|
||||
});
|
||||
},
|
||||
}],
|
||||
publicDir: resolve("apps/web/public"),
|
||||
root: process.cwd(),
|
||||
server: { host: "127.0.0.1", port: 0 },
|
||||
});
|
||||
await vite.listen();
|
||||
const address = vite.httpServer?.address();
|
||||
if (!address || typeof address === "string") throw new Error("Vite did not expose a test port.");
|
||||
webUrl = `http://127.0.0.1:${address.port}`;
|
||||
});
|
||||
|
||||
test.afterAll(async () => vite.close());
|
||||
|
||||
test("TDD-WP5-04 enumerates no preview or private client state", async ({ context, page }) => {
|
||||
requestCounts.preview = 0;
|
||||
requestCounts.private = 0;
|
||||
requestCounts.public = 0;
|
||||
await page.goto(`${webUrl}/tests/e2e/fixtures/public-asset-cache.html`);
|
||||
await expect(page.locator("#status")).toHaveText("ready");
|
||||
await page.reload();
|
||||
await expect(page.locator("#status")).toHaveText("ready");
|
||||
|
||||
const online = await page.evaluate(async ({ privateId, previewId, publicId, releaseVersion }) => {
|
||||
const cache = window.dadaCacheProbe.cache;
|
||||
await cache.clear();
|
||||
const cached = await cache.cache({
|
||||
access_class: "public_release_asset",
|
||||
cache_kind: "thumbnail",
|
||||
release_version: releaseVersion,
|
||||
resource_id: publicId,
|
||||
});
|
||||
const rejected = await Promise.all([
|
||||
cache.cache({ access_class: "internal_preview_asset", cache_kind: "thumbnail", release_version: releaseVersion, resource_id: previewId }),
|
||||
cache.cache({ access_class: "private_user_asset", cache_kind: "thumbnail", release_version: releaseVersion, resource_id: privateId }),
|
||||
]);
|
||||
const preview = await fetch(`/api/v1/assets/preview/${releaseVersion}/${previewId}`);
|
||||
const privateAsset = await fetch(`/api/v1/private-assets/${releaseVersion}/${privateId}`);
|
||||
const inspection = await cache.inspect();
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
const databases = await indexedDB.databases();
|
||||
return {
|
||||
cached,
|
||||
inspection,
|
||||
private_bytes: (await privateAsset.arrayBuffer()).byteLength,
|
||||
private_cache_control: privateAsset.headers.get("cache-control"),
|
||||
preview_bytes: (await preview.arrayBuffer()).byteLength,
|
||||
preview_cache_control: preview.headers.get("cache-control"),
|
||||
rejected,
|
||||
service_workers: registrations.map((registration) => ({
|
||||
active: registration.active?.state,
|
||||
scope: registration.scope,
|
||||
script_url: registration.active?.scriptURL,
|
||||
})),
|
||||
indexed_db_names: databases.map((database) => database.name).filter(Boolean).sort(),
|
||||
local_storage_keys: Object.keys(localStorage),
|
||||
session_storage_keys: Object.keys(sessionStorage),
|
||||
};
|
||||
}, { privateId, previewId, publicId, releaseVersion });
|
||||
|
||||
await context.setOffline(true);
|
||||
const offline = await page.evaluate(async ({ privateId, previewId, publicId, releaseVersion }) => {
|
||||
const read = async (url: string) => {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
return { body: await response.text(), status: response.status };
|
||||
} catch {
|
||||
return { body: null, status: "network_error" };
|
||||
}
|
||||
};
|
||||
return {
|
||||
preview: await read(`/api/v1/assets/preview/${releaseVersion}/${previewId}`),
|
||||
private: await read(`/api/v1/private-assets/${releaseVersion}/${privateId}`),
|
||||
public: await read(`/api/v1/assets/public/${releaseVersion}/${publicId}`),
|
||||
};
|
||||
}, { privateId, previewId, publicId, releaseVersion });
|
||||
await context.setOffline(false);
|
||||
|
||||
expect(online.cached.status).toBe("cached");
|
||||
expect(online.rejected).toEqual([
|
||||
{ status: "rejected_not_allowlisted" },
|
||||
{ status: "rejected_not_allowlisted" },
|
||||
]);
|
||||
expect(online.preview_cache_control).toBe("private, no-store");
|
||||
expect(online.private_cache_control).toBe("private, no-store");
|
||||
expect(online.inspection.cache_keys).toHaveLength(1);
|
||||
expect(online.inspection.cache_names).toEqual(["dada-public-assets-v1"]);
|
||||
expect(online.inspection.entries).toEqual([expect.objectContaining({ resource_id: publicId })]);
|
||||
expect(JSON.stringify(online.inspection)).not.toContain(previewId);
|
||||
expect(JSON.stringify(online.inspection)).not.toContain(privateId);
|
||||
expect(online.indexed_db_names).toEqual(["dada-public-asset-cache-v1"]);
|
||||
expect(online.local_storage_keys).toEqual([]);
|
||||
expect(online.session_storage_keys).toEqual([]);
|
||||
expect(online.service_workers).toHaveLength(1);
|
||||
expect(offline.public).toEqual({ body: "public-content", status: 200 });
|
||||
expect(offline.preview.status).toBe("network_error");
|
||||
expect(offline.private.status).toBe("network_error");
|
||||
expect(requestCounts).toEqual({ preview: 1, private: 1, public: 1 });
|
||||
|
||||
const cacheEnumeration = {
|
||||
business_database_calls: 0,
|
||||
offline,
|
||||
online,
|
||||
origin_request_counts: { ...requestCounts },
|
||||
};
|
||||
writeEvidence(process.env.DADA_EVIDENCE_DIR_WP5_CACHE, "cache-enumeration.json", cacheEnumeration);
|
||||
writeEvidence(process.env.DADA_EVIDENCE_DIR_WP5_CACHE, "service-worker.json", { registrations: online.service_workers });
|
||||
writeEvidence(process.env.DADA_EVIDENCE_DIR_WP5_RES, "cache-enumeration.json", cacheEnumeration);
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { createServer, type ViteDevServer } from "vite";
|
||||
|
||||
let vite: ViteDevServer;
|
||||
let webUrl: string;
|
||||
const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAEklEQVQImWO4E6XxHxkzEBQAANIxHF3ECQOzAAAAAElFTkSuQmCC", "base64");
|
||||
const adminSession = { csrf_token: "csrf-wp5-05-admin-000000000000000000000000000000000" };
|
||||
const projectId = "00000000-0000-4000-8000-000000001405";
|
||||
const userSession = {
|
||||
audience: "user", authenticated: true, credits: { available_balance: 10, reserved_balance: 0 },
|
||||
csrf_token: "csrf-wp5-05-user-0000000000000000000000000000000000",
|
||||
expires_at: "2026-09-03T12:00:00.000Z",
|
||||
user: { creator_name: "Sticker User", role: "user", social_id: "@sticker", status: "active", user_id: projectId },
|
||||
};
|
||||
|
||||
function asset(stableId = "STK1408") {
|
||||
return {
|
||||
enabled: true, file_state: "committed", height: 3, mime: "image/png", mime_type: "image/png", order: 184,
|
||||
original_byte_size: png.byteLength, original_filename: `${stableId}.png`, original_reference: `/api/v1/assets/public/asset-20260803.1/${stableId}`,
|
||||
origin: "admin_uploaded", part: 25, relative_path: `managed-assets/stickers/original/${stableId}.png`, resource_version: "asset-20260803.1",
|
||||
sha256: "0".repeat(64), stable_id: stableId, thumbnail_byte_size: 75,
|
||||
thumbnail_reference: { media: "thumbnail", resource_id: stableId, resource_version: "asset-20260803.1", url: `/api/v1/assets/public/asset-20260803.1/${stableId}?variant=thumbnail` }, width: 4,
|
||||
};
|
||||
}
|
||||
|
||||
function assetsResponse(status: "active" | "full" | "unavailable" = "active", items = [asset()]) {
|
||||
return {
|
||||
count: items.length, items, release_version: items.length ? "asset-20260803.1" : null,
|
||||
storage: { capacity_notice_level: status === "active" ? "normal" : "critical", hard_limit_bytes: 5_368_709_120, managed_content_bytes: status === "full" ? 5_368_709_120 : 150, storage_status: status },
|
||||
};
|
||||
}
|
||||
|
||||
test.beforeAll(async () => {
|
||||
vite = await createServer({ configFile: resolve("apps/web/vite.config.ts"), root: resolve("apps/web"), server: { host: "127.0.0.1", port: 0 } });
|
||||
await vite.listen();
|
||||
const address = vite.httpServer?.address();
|
||||
if (!address || typeof address === "string") throw new Error("Vite did not expose a test port.");
|
||||
webUrl = `http://127.0.0.1:${address.port}`;
|
||||
});
|
||||
|
||||
test.afterAll(async () => vite.close());
|
||||
|
||||
async function routeAdminSession(page: Page) {
|
||||
await page.route("**/api/v1/admin-auth/session", (route) => route.fulfill({ body: JSON.stringify(adminSession), contentType: "application/json", status: 200 }));
|
||||
await page.route("**/api/v1/assets/public/**", (route) => route.fulfill({ body: png, contentType: "image/png", status: 200 }));
|
||||
}
|
||||
|
||||
test("TDD-WP5-UPL-001-upload-metering uploads and publishes with stable metadata controls", async ({ page }) => {
|
||||
await routeAdminSession(page);
|
||||
let uploaded = false;
|
||||
let multipartBody = "";
|
||||
await page.route("**/api/v1/admin/assets/static-stickers", async (route) => {
|
||||
if (route.request().method() === "POST") {
|
||||
multipartBody = route.request().postDataBuffer()?.toString("latin1") ?? "";
|
||||
uploaded = true;
|
||||
return route.fulfill({ body: JSON.stringify({ created: true, item: asset(), release_version: "asset-20260803.1" }), contentType: "application/json", status: 201 });
|
||||
}
|
||||
return route.fulfill({ body: JSON.stringify(assetsResponse("active", uploaded ? [asset()] : [])), contentType: "application/json", status: 200 });
|
||||
});
|
||||
|
||||
await page.goto(`${webUrl}/admin/assets`);
|
||||
await expect(page.getByRole("heading", { name: "普通贴纸" })).toBeVisible();
|
||||
await expect(page.getByText("当前没有后台上传的普通贴纸。")).toBeVisible();
|
||||
await page.getByLabel("贴纸文件").setInputFiles({ buffer: png, mimeType: "image/png", name: "STK1408.png" });
|
||||
await page.getByRole("button", { name: "上传并发布" }).click();
|
||||
await expect(page.getByRole("rowheader", { name: /STK1408/ })).toBeVisible();
|
||||
expect(multipartBody).toContain("STK1408");
|
||||
expect(multipartBody).toContain("image/png");
|
||||
expect(multipartBody).toContain("original_sha256");
|
||||
const evidenceRoot = process.env.DADA_EVIDENCE_DIR_WP5_UPL;
|
||||
if (evidenceRoot) {
|
||||
const directory = resolve(evidenceRoot, "screenshots");
|
||||
mkdirSync(directory, { recursive: true });
|
||||
await page.screenshot({ fullPage: true, path: resolve(directory, "admin-assets-uploaded.png") });
|
||||
}
|
||||
});
|
||||
|
||||
test("storage full disables every upload control and load failure exposes retry", async ({ page }) => {
|
||||
await page.setViewportSize({ height: 844, width: 390 });
|
||||
await routeAdminSession(page);
|
||||
await page.route("**/api/v1/admin/assets/static-stickers", (route) => route.fulfill({ body: JSON.stringify(assetsResponse("full")), contentType: "application/json", status: 200 }));
|
||||
await page.goto(`${webUrl}/admin/assets`);
|
||||
await expect(page.getByText("当前存储状态禁止新增原图和缩略图。")).toBeVisible();
|
||||
await expect(page.getByLabel("贴纸文件")).toBeDisabled();
|
||||
await expect(page.getByRole("button", { name: "上传并发布" })).toBeDisabled();
|
||||
const evidenceRoot = process.env.DADA_EVIDENCE_DIR_WP5_UPL;
|
||||
if (evidenceRoot) {
|
||||
const directory = resolve(evidenceRoot, "screenshots");
|
||||
mkdirSync(directory, { recursive: true });
|
||||
await page.screenshot({ fullPage: true, path: resolve(directory, "admin-assets-full-mobile.png") });
|
||||
}
|
||||
|
||||
await page.unroute("**/api/v1/admin/assets/static-stickers");
|
||||
await page.route("**/api/v1/admin/assets/static-stickers", (route) => route.fulfill({ status: 503 }));
|
||||
await page.reload();
|
||||
await expect(page.getByRole("alert")).toContainText("素材状态暂时无法读取");
|
||||
await expect(page.getByRole("button", { name: "重试" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("the editor merges the current uploaded release and saves its resource version", async ({ page }) => {
|
||||
let savedResourceVersion = "";
|
||||
let originalRequests = 0;
|
||||
await page.route("**/api/v1/auth/session", (route) => route.fulfill({ body: JSON.stringify(userSession), contentType: "application/json", status: 200 }));
|
||||
await page.route("**/api/v1/static-stickers/current", (route) => route.fulfill({ body: JSON.stringify({ count: 1, items: [asset()], release_version: "asset-20260803.1" }), contentType: "application/json", status: 200 }));
|
||||
await page.route(`**/api/v1/projects/${projectId}`, (route) => route.fulfill({ body: JSON.stringify({
|
||||
canvas_state: { background: { adjustments: { brightness: 0, contrast: 0, crop: null, filter: "none", fit: "fill", saturation: 0, sharpness: 0, temperature: 0 }, asset_id: null }, elements: [], pixel_height: 1440, pixel_width: 1080, ratio: "3:4", schema_version: 1 },
|
||||
created_at: "2026-08-03T12:00:00.000Z", current_image_id: null, images: [], name: "上传贴纸", project_id: projectId, ratio: "3:4", state_version: 1,
|
||||
}), contentType: "application/json", status: 200 }));
|
||||
await page.route(`**/api/v1/projects/${projectId}/state`, async (route) => {
|
||||
const body = route.request().postDataJSON() as { canvas_state: { elements: Array<{ resource_version: string }> } };
|
||||
savedResourceVersion = body.canvas_state.elements[0]?.resource_version ?? "";
|
||||
await route.fulfill({ body: JSON.stringify({ save_status: "saved", state_version: 2 }), contentType: "application/json", status: 200 });
|
||||
});
|
||||
await page.route("**/api/v1/assets/recent?asset_kind=text_template", (route) => route.fulfill({ body: JSON.stringify({ items: [] }), contentType: "application/json", status: 200 }));
|
||||
await page.route("**/api/v1/assets/public/**", async (route) => {
|
||||
if (!new URL(route.request().url()).searchParams.has("variant")) originalRequests += 1;
|
||||
await route.fulfill({ body: png, contentType: "image/png", status: 200 });
|
||||
});
|
||||
|
||||
await page.goto(`${webUrl}/app/projects/${projectId}/editor`);
|
||||
await page.getByRole("button", { name: "普通贴纸", exact: true }).click();
|
||||
await expect(page.getByText("共 1,408 张", { exact: true })).toBeVisible();
|
||||
const list = page.getByTestId("static-sticker-list");
|
||||
await list.evaluate((element) => { element.scrollTop = element.scrollHeight; element.dispatchEvent(new Event("scroll")); });
|
||||
await list.getByRole("button", { name: "添加贴纸 STK1408" }).click();
|
||||
await expect.poll(() => savedResourceVersion).toBe("asset-20260803.1");
|
||||
await expect.poll(() => originalRequests).toBeGreaterThan(0);
|
||||
});
|
||||
Reference in New Issue
Block a user