Complete V1 RC deployment capability gates
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#202124" />
|
||||
<title>Blender Web Editor</title>
|
||||
<title>Web Blender Modeler V1</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -302,7 +302,7 @@ function ViewportPlaceholder({ snapshot, geometryBuffers, nonMeshGeometryBuffers
|
||||
<canvas ref={canvasRef} className="viewport-canvas" aria-label="Three.js WebGL2 视口" />
|
||||
<div className="axis-gizmo" aria-hidden="true"><span className="axis-x">X</span><span className="axis-y">Y</span><span className="axis-z">Z</span></div>
|
||||
{viewportError || !snapshot || snapshot.nodes.length === 0 ? <div className="viewport-message">
|
||||
<strong>Blender Web Viewport</strong>
|
||||
<strong>3D Viewport</strong>
|
||||
{viewportError ? <span>WebGL 不可用,已保留场景编辑界面:{viewportError}</span> : <span>Three.js WebGL2 适配器</span>}
|
||||
</div> : null}
|
||||
<div className="viewport-toolbar" aria-label="视口工具">
|
||||
@@ -1344,7 +1344,7 @@ export function App() {
|
||||
return (
|
||||
<main className="blender-app" data-workspace={workspace} data-ui-revision={uiState.context.revision}>
|
||||
<header className="topbar">
|
||||
<div className="brand"><span className="brand-mark" aria-hidden="true">◈</span><span>Blender Web</span></div>
|
||||
<div className="brand"><span className="brand-mark" aria-hidden="true">◈</span><span>Web Blender Modeler V1</span></div>
|
||||
<nav className="menu-bar" aria-label="主菜单"><button type="button" onClick={() => fileInputRef.current?.click()}>文件</button><button type="button">编辑</button><button type="button">渲染</button><button type="button">窗口</button><button type="button">帮助</button></nav>
|
||||
<nav className="workspace-tabs" aria-label="工作区">
|
||||
{(["Layout", "Modeling", "Animation"] as WorkspaceId[]).map((item) => <button key={item} className={item === workspace ? "workspace-tab active" : "workspace-tab"} type="button" onClick={() => dispatchUI({ type: "switchWorkspace", workspaceId: item })}>{item}</button>)}
|
||||
@@ -1360,7 +1360,7 @@ export function App() {
|
||||
<Area className="timeline-area" editor="Timeline"><Timeline snapshot={snapshot} frame={frame} start={frameRange.start} end={frameRange.end} onFrameChange={(value) => void applyEditCommand({ type: "setFrame", frame: value })} onCommand={(command) => void applyEditCommand(command)} /></Area>
|
||||
</div>
|
||||
{uiState.operatorSearchOpen ? <OperatorSearch commands={operatorCommands} onClose={() => dispatchUI({ type: "toggleOperatorSearch", open: false })} /> : null}
|
||||
<footer className="status-bar"><span>Blender Web 0.1.0</span><span data-testid="scene-stats">Objects {objectCount} · Vertices {vertexCount} · Faces {faceCount}</span>{openProgress ? <span data-testid="open-progress">{openProgress.message ?? "Opening"}</span> : null}<span className="status-spacer" /><span>{manifestStatus}</span><span>{wasmStatus}</span><span data-testid="engine-status">{engineStatus}</span><span>{storageStatus}</span></footer>
|
||||
<footer className="status-bar"><span>Web Blender Modeler V1</span><span data-testid="scene-stats">Objects {objectCount} · Vertices {vertexCount} · Faces {faceCount}</span>{openProgress ? <span data-testid="open-progress">{openProgress.message ?? "Opening"}</span> : null}<span className="status-spacer" /><span>{manifestStatus}</span><span>{wasmStatus}</span><span data-testid="engine-status">{engineStatus}</span><span>{storageStatus}</span></footer>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
export interface BrowserCapabilities {
|
||||
import { blockedGate, capabilityIssue, readyGate, type CapabilityGateResult } from "../../../protocol/capability-gates";
|
||||
|
||||
export interface WasmThreadingCapabilities {
|
||||
crossOriginIsolated: boolean;
|
||||
sharedArrayBuffer: boolean;
|
||||
worker: boolean;
|
||||
}
|
||||
|
||||
export interface BrowserCapabilities extends WasmThreadingCapabilities {
|
||||
webgl2: boolean;
|
||||
offscreenCanvas: boolean;
|
||||
opfs: boolean;
|
||||
sharedArrayBuffer: boolean;
|
||||
worker: boolean;
|
||||
wasm: boolean;
|
||||
wasmSimd: boolean;
|
||||
wasmThreads: boolean;
|
||||
@@ -11,6 +17,34 @@ export interface BrowserCapabilities {
|
||||
indexedDb: boolean;
|
||||
}
|
||||
|
||||
export function gateWasmThreadingCapability(capabilities: WasmThreadingCapabilities): CapabilityGateResult {
|
||||
const issues = [];
|
||||
if (!capabilities.crossOriginIsolated) {
|
||||
issues.push(capabilityIssue(
|
||||
"PLATFORM_CAPABILITY_UNAVAILABLE",
|
||||
"Cross-origin isolation is required for the pthread WASM engine",
|
||||
"crossOriginIsolated",
|
||||
));
|
||||
}
|
||||
if (!capabilities.sharedArrayBuffer) {
|
||||
issues.push(capabilityIssue(
|
||||
"PLATFORM_CAPABILITY_UNAVAILABLE",
|
||||
"SharedArrayBuffer is required for the pthread WASM engine",
|
||||
"sharedArrayBuffer",
|
||||
));
|
||||
}
|
||||
if (!capabilities.worker) {
|
||||
issues.push(capabilityIssue(
|
||||
"PLATFORM_CAPABILITY_UNAVAILABLE",
|
||||
"Worker is required for the pthread WASM engine",
|
||||
"worker",
|
||||
));
|
||||
}
|
||||
return issues.length > 0
|
||||
? blockedGate("M6-02", "WASM_PTHREAD_ENGINE", issues)
|
||||
: readyGate("M6-02", "WASM_PTHREAD_ENGINE");
|
||||
}
|
||||
|
||||
function supportsWasmFeature(feature: "simd" | "threads"): boolean {
|
||||
if (typeof WebAssembly === "undefined") return false;
|
||||
|
||||
@@ -40,6 +74,7 @@ export function detectBrowserCapabilities(): BrowserCapabilities {
|
||||
const storage = typeof navigator === "undefined" ? undefined : navigator.storage;
|
||||
|
||||
return {
|
||||
crossOriginIsolated: globalThis.crossOriginIsolated === true,
|
||||
webgl2,
|
||||
offscreenCanvas: typeof OffscreenCanvas !== "undefined",
|
||||
opfs: Boolean(storage?.getDirectory),
|
||||
|
||||
@@ -135,6 +135,10 @@ export class StorageClient {
|
||||
return this.request({ type: "listSimulationCaches", projectId }) as Promise<StorageSimulationCacheListResult>;
|
||||
}
|
||||
|
||||
getPendingRequestCount(): number {
|
||||
return this.pending.size;
|
||||
}
|
||||
|
||||
private request(command: StorageRequest["command"], transfer: Transferable[] = []): Promise<NonNullable<StorageResponse["result"]>> {
|
||||
const requestId = `storage-${++this.counter}`;
|
||||
const request: StorageRequest = { requestId, command };
|
||||
|
||||
@@ -18,7 +18,7 @@ type Request =
|
||||
type Response =
|
||||
| { type: "ready"; index: { stripCount: number; bucketCount: number; referenceCount: number; estimatedBytes: number } }
|
||||
| { type: "seekResult"; requestId: string; result: LongMediaSeekResultIR }
|
||||
| { type: "disposed"; cacheBytes: number }
|
||||
| { type: "disposed"; releasedCacheBytes: number; cacheBytesAfter: number }
|
||||
| { type: "error"; requestId?: string; message: string };
|
||||
|
||||
const scope = self as unknown as {
|
||||
@@ -71,9 +71,9 @@ scope.onmessage = (event): void => {
|
||||
return;
|
||||
}
|
||||
if (message.type === "cancel") { session?.cancel(); return; }
|
||||
const cacheBytes = session?.cache.stats().bytes ?? 0;
|
||||
const releasedCacheBytes = session?.cache.stats().bytes ?? 0;
|
||||
session?.dispose();
|
||||
session = null;
|
||||
assets.clear();
|
||||
scope.postMessage({ type: "disposed", cacheBytes });
|
||||
scope.postMessage({ type: "disposed", releasedCacheBytes, cacheBytesAfter: 0 });
|
||||
};
|
||||
|
||||
@@ -4,12 +4,13 @@ import react from "@vitejs/plugin-react";
|
||||
import fs from "node:fs";
|
||||
// @ts-expect-error The runtime is Node; this project intentionally avoids a browser dependency on Node types.
|
||||
import path from "node:path";
|
||||
// @ts-expect-error The runtime is Node; this project intentionally avoids a browser dependency on Node types.
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const isolationHeaders = {
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||
"Cross-Origin-Resource-Policy": "same-origin",
|
||||
};
|
||||
const deploymentContract = JSON.parse(
|
||||
fs.readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../docs/web/deployment-contract.json"), "utf8"),
|
||||
) as { responseHeaders: { allResponses: Record<string, string> } };
|
||||
const isolationHeaders = deploymentContract.responseHeaders.allResponses;
|
||||
|
||||
function preserveIsolationHeaders(): Plugin {
|
||||
const install = (server: { middlewares: { use: (handler: (request: unknown, response: { setHeader: (name: string, value: string) => void }, next: () => void) => void) => void } }) => {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
project(blender_web_engine LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
set(WITH_WEB ON CACHE BOOL "Build the browser Blender engine")
|
||||
option(WEB_ENGINE_THREADS "Build the pthread variant" OFF)
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../blender-5.2.0/build_files/cmake/platform/web.cmake")
|
||||
@@ -16,6 +20,7 @@ add_library(blender_web_engine_core STATIC
|
||||
)
|
||||
target_include_directories(blender_web_engine_core PUBLIC
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../../blender-5.2.0/source/blender/web_engine"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../../blender-5.2.0/extern/json/include"
|
||||
)
|
||||
|
||||
add_executable(web_engine web_engine_smoke.cpp)
|
||||
|
||||
@@ -64,6 +64,7 @@ bool web_engine_blend_main_create_primitive(WebBlendMainState *,
|
||||
bool web_engine_blend_main_duplicate_object(WebBlendMainState *,
|
||||
const char *,
|
||||
const std::vector<float> &,
|
||||
bool,
|
||||
std::string &,
|
||||
std::string &error)
|
||||
{
|
||||
@@ -98,6 +99,270 @@ bool web_engine_blend_main_translate_vertices(WebBlendMainState *,
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool native_main_unavailable(std::string &error)
|
||||
{
|
||||
error = "native smoke Main stub";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
#define WEB_NATIVE_MAIN_STUB(name, ...) \
|
||||
bool name(__VA_ARGS__, std::string &error) \
|
||||
{ \
|
||||
return native_main_unavailable(error); \
|
||||
}
|
||||
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_modifier_visibility,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
bool,
|
||||
bool,
|
||||
bool,
|
||||
bool)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_sculpt_attributes,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const std::vector<float> &,
|
||||
const std::vector<uint32_t> &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_sculpt_stroke, WebBlendMainState *, const char *)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_mesh_edit,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
const char *,
|
||||
const std::vector<uint32_t> &,
|
||||
const std::vector<float> &,
|
||||
float,
|
||||
int)
|
||||
WEB_NATIVE_MAIN_STUB(
|
||||
web_engine_blend_main_create_uv_map, WebBlendMainState *, const char *, const char *)
|
||||
WEB_NATIVE_MAIN_STUB(
|
||||
web_engine_blend_main_set_active_uv_map, WebBlendMainState *, const char *, const char *)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_unwrap_uv,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const std::vector<uint32_t> &,
|
||||
const char *)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_add_material_slot,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
std::string &)
|
||||
WEB_NATIVE_MAIN_STUB(
|
||||
web_engine_blend_main_remove_material_slot, WebBlendMainState *, const char *, int)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_assign_material_faces,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const std::vector<uint32_t> &,
|
||||
int)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_material_principled,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const std::vector<float> &,
|
||||
float,
|
||||
float,
|
||||
const std::vector<float> &,
|
||||
float,
|
||||
float,
|
||||
float,
|
||||
float,
|
||||
float,
|
||||
float,
|
||||
float)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_import_image,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
int,
|
||||
int,
|
||||
const char *,
|
||||
std::string &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_material_image,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
const char *,
|
||||
const char *)
|
||||
WEB_NATIVE_MAIN_STUB(
|
||||
web_engine_blend_main_set_shader_graph, WebBlendMainState *, const char *, const char *)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_insert_keyframe,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
int,
|
||||
const char *,
|
||||
const char *)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_delete_keyframe,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
int,
|
||||
const char *)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_fcurve_interpolation,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
const char *)
|
||||
WEB_NATIVE_MAIN_STUB(
|
||||
web_engine_blend_main_set_active_action, WebBlendMainState *, const char *, const char *)
|
||||
WEB_NATIVE_MAIN_STUB(
|
||||
web_engine_blend_main_set_nla_stack, WebBlendMainState *, const char *, const char *)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_constraint,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
bool,
|
||||
float)
|
||||
WEB_NATIVE_MAIN_STUB(
|
||||
web_engine_blend_main_set_parent, WebBlendMainState *, const char *, const char *, bool)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_create_collection,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
std::string &)
|
||||
WEB_NATIVE_MAIN_STUB(
|
||||
web_engine_blend_main_move_to_collection, WebBlendMainState *, const char *, const char *)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_rename_id,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
std::string &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_join_objects,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const std::vector<std::string> &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_separate_faces,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const std::vector<uint32_t> &,
|
||||
const char *,
|
||||
std::string &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_apply_transform, WebBlendMainState *, const char *)
|
||||
WEB_NATIVE_MAIN_STUB(
|
||||
web_engine_blend_main_set_origin, WebBlendMainState *, const char *, const char *)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_create_curve,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
const std::vector<float> &,
|
||||
bool,
|
||||
int,
|
||||
std::string &,
|
||||
std::string &)
|
||||
WEB_NATIVE_MAIN_STUB(
|
||||
web_engine_blend_main_delete_non_mesh_data, WebBlendMainState *, const char *)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_curve_control_points,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const std::vector<float> &,
|
||||
const std::vector<uint32_t> &,
|
||||
int)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_curve_handle,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
uint32_t,
|
||||
bool,
|
||||
const std::vector<float> &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_curve_topology,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const WebCurveTopologyEdit &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_curve_splines,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const std::vector<WebCurveSplineEdit> &,
|
||||
const std::vector<float> &,
|
||||
const std::vector<float> &,
|
||||
const std::vector<int> &,
|
||||
const std::vector<float> &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_surface_topology,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const std::vector<WebSurfaceSplineEdit> &,
|
||||
const std::vector<float> &,
|
||||
const std::vector<float> &)
|
||||
WEB_NATIVE_MAIN_STUB(
|
||||
web_engine_blend_main_set_font_body, WebBlendMainState *, const char *, const char *)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_font_properties,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const WebFontPropertiesEdit &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_font_advanced,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const std::vector<WebFontCharacterEdit> &,
|
||||
const std::vector<WebFontTextBoxEdit> &,
|
||||
int)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_font_links,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const std::array<std::string, 4> &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_volume_properties,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const WebVolumePropertiesEdit &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_metaball_elements,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const std::vector<WebMetaballElementEdit> &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_grease_pencils_json,
|
||||
WebBlendMainState *,
|
||||
std::string &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_physics_simulation_json,
|
||||
WebBlendMainState *,
|
||||
std::string &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_create_grease_pencil_layer,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_remove_grease_pencil_layer,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_move_grease_pencil_layer,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
const char *)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_insert_grease_pencil_frame,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
int,
|
||||
int)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_remove_grease_pencil_frame,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
int)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_grease_pencil_strokes,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
int,
|
||||
const std::vector<WebGreasePencilStrokeEdit> &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_vertex_colors,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
const char *,
|
||||
const std::vector<uint32_t> &,
|
||||
const std::vector<float> &)
|
||||
WEB_NATIVE_MAIN_STUB(web_engine_blend_main_set_vertex_weights,
|
||||
WebBlendMainState *,
|
||||
const char *,
|
||||
const char *,
|
||||
const std::vector<uint32_t> &,
|
||||
const std::vector<float> &,
|
||||
bool,
|
||||
bool)
|
||||
WEB_NATIVE_MAIN_STUB(
|
||||
web_engine_blend_main_set_render_properties, WebBlendMainState *, const char *, const char *)
|
||||
|
||||
#undef WEB_NATIVE_MAIN_STUB
|
||||
|
||||
bool web_engine_blend_main_write(WebBlendMainState *, std::vector<uint8_t> &, std::string &error)
|
||||
{
|
||||
error = "native smoke Main stub";
|
||||
|
||||
6
web/package-lock.json
generated
6
web/package-lock.json
generated
@@ -1533,9 +1533,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.17",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz",
|
||||
"integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==",
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
@@ -84,7 +84,11 @@
|
||||
"release:evidence-long-media": "node ../tools/web/collect-release-evidence.mjs --record-long-media",
|
||||
"release:evidence-oom": "node ../tools/web/collect-release-evidence.mjs --record-oom-recovery",
|
||||
"test:release-evidence": "node ../tools/web/check-release-evidence.mjs",
|
||||
"test:v1-acceptance-coverage": "node ../tools/web/check-v1-acceptance-coverage.mjs",
|
||||
"test:v1-acceptance-evidence": "node ../tools/web/check-v1-acceptance-evidence.mjs",
|
||||
"test:v1-acceptance-release-binding": "node ../tools/web/check-v1-acceptance-release-binding.mjs",
|
||||
"test:status-consistency": "node ../tools/web/check-status-consistency.mjs",
|
||||
"release:v1-acceptance": "node ../tools/web/run-v1-acceptance.mjs",
|
||||
"release:offline": "npm run build && node ../tools/web/check-offline-reproducibility.mjs",
|
||||
"diagnose:depsgraph": "node ../tools/web/diagnose-depsgraph.mjs"
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface CapabilityIssue {
|
||||
}
|
||||
|
||||
export interface CapabilityGateResult {
|
||||
taskId: "N-011" | "N-012" | "N-013" | "N-014" | "N-015" | "N-018" | "N-020" | "N-021" | "N-022" | "N-023" | "N-024" | "N-025" | "N-026" | "PBR-007" | "PBR-008" | "PBR-009" | "PBR-010" | "PBR-011" | "PBR-012";
|
||||
taskId: "M6-02" | "N-011" | "N-012" | "N-013" | "N-014" | "N-015" | "N-018" | "N-020" | "N-021" | "N-022" | "N-023" | "N-024" | "N-025" | "N-026" | "PBR-007" | "PBR-008" | "PBR-009" | "PBR-010" | "PBR-011" | "PBR-012";
|
||||
capability: string;
|
||||
status: "READY" | "BLOCKED";
|
||||
issues: CapabilityIssue[];
|
||||
|
||||
@@ -319,11 +319,13 @@ test("streams ten-million-triangle SceneIR ranges into a bounded interactive LOD
|
||||
const recoveryVisible = visiblePixels(recovery);
|
||||
recovery.dispose();
|
||||
recoveryCanvas.remove();
|
||||
const afterHeapBytes = heap();
|
||||
|
||||
return {
|
||||
elapsedMs: Math.round(performance.now() - started),
|
||||
baselineHeapBytes,
|
||||
peakHeapBytes,
|
||||
afterHeapBytes,
|
||||
wasmAllocatedBytes: wasm.allocatedBytes,
|
||||
invalidCode,
|
||||
cancelled,
|
||||
@@ -342,6 +344,11 @@ test("streams ten-million-triangle SceneIR ranges into a bounded interactive LOD
|
||||
chunksAtFirstInteractive: result.completed.viewport?.chunksAtFirstInteractive,
|
||||
estimatedGpuBytes: result.completed.viewport?.estimatedGpuBytes,
|
||||
wasmAllocatedBytes: result.wasmAllocatedBytes,
|
||||
baselineHeapBytes: result.baselineHeapBytes,
|
||||
peakHeapBytes: result.peakHeapBytes,
|
||||
afterHeapBytes: result.afterHeapBytes,
|
||||
cacheDeleted: result.completed.viewport?.cache.deleted,
|
||||
recoveryVisible: result.recoveryVisible,
|
||||
cancelLatencyMs: result.cancelled.cancelLatencyMs,
|
||||
elapsedMs: result.elapsedMs,
|
||||
}));
|
||||
|
||||
@@ -76,7 +76,7 @@ test("indexes, seeks, cancels and reopens a bounded one-million-frame media time
|
||||
init(timelineValue: unknown, assets: Array<{ sourceId: string; data: ArrayBuffer }>): Promise<{ stripCount: number; bucketCount: number; referenceCount: number; estimatedBytes: number }>;
|
||||
seek(frame: number, decodeDelayMs?: number): Promise<import("/src/sequencer/LongMediaTimeline.ts").LongMediaSeekResultIR>;
|
||||
cancel(): void;
|
||||
dispose(): Promise<number>;
|
||||
dispose(): Promise<{ releasedCacheBytes: number; cacheBytesAfter: number }>;
|
||||
terminate(): void;
|
||||
}
|
||||
const createWorkerClient = (): WorkerClient => {
|
||||
@@ -84,12 +84,12 @@ test("indexes, seeks, cancels and reopens a bounded one-million-frame media time
|
||||
let counter = 0;
|
||||
let readyResolve: ((value: { stripCount: number; bucketCount: number; referenceCount: number; estimatedBytes: number }) => void) | undefined;
|
||||
let readyReject: ((reason: Error) => void) | undefined;
|
||||
let disposeResolve: ((bytes: number) => void) | undefined;
|
||||
let disposeResolve: ((status: { releasedCacheBytes: number; cacheBytesAfter: number }) => void) | undefined;
|
||||
const pending = new Map<string, { resolve: (value: import("/src/sequencer/LongMediaTimeline.ts").LongMediaSeekResultIR) => void; reject: (reason: Error) => void }>();
|
||||
worker.onmessage = (event: MessageEvent<any>) => {
|
||||
const message = event.data;
|
||||
if (message.type === "ready") { readyResolve?.(message.index); readyResolve = undefined; readyReject = undefined; return; }
|
||||
if (message.type === "disposed") { disposeResolve?.(message.cacheBytes); disposeResolve = undefined; return; }
|
||||
if (message.type === "disposed") { disposeResolve?.({ releasedCacheBytes: message.releasedCacheBytes, cacheBytesAfter: message.cacheBytesAfter }); disposeResolve = undefined; return; }
|
||||
if (message.type === "error") {
|
||||
if (message.requestId) { pending.get(message.requestId)?.reject(new Error(message.message)); pending.delete(message.requestId); }
|
||||
else { readyReject?.(new Error(message.message)); readyResolve = undefined; readyReject = undefined; }
|
||||
@@ -164,7 +164,7 @@ test("indexes, seeks, cancels and reopens a bounded one-million-frame media time
|
||||
const serialized = serializeLongMediaSessionManifest(manifest);
|
||||
const manifestSha256 = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", serialized.slice(0))), (value) => value.toString(16).padStart(2, "0")).join("");
|
||||
const storedSession = await writer.putAsset(projectId, serialized, "application/vnd.blender.long-media+json", "cache/long-media-session.json");
|
||||
const cacheBytesBeforeDispose = await client.dispose();
|
||||
const disposed = await client.dispose();
|
||||
client.terminate();
|
||||
writer.terminate();
|
||||
|
||||
@@ -180,7 +180,7 @@ test("indexes, seeks, cancels and reopens a bounded one-million-frame media time
|
||||
{ sourceId: "media:audio", data: reopenedAudio.data },
|
||||
]);
|
||||
const reopenedSeek = await restarted.seek(reopenedManifest.currentFrame);
|
||||
const restartedCacheBytes = await restarted.dispose();
|
||||
const restartedDisposed = await restarted.dispose();
|
||||
restarted.terminate();
|
||||
|
||||
const codec = gateSequencerCodec("video/mp4", new Set(["image/png", "audio/wav"]));
|
||||
@@ -207,8 +207,8 @@ test("indexes, seeks, cancels and reopens a bounded one-million-frame media time
|
||||
randomSeekMs,
|
||||
latest: [superseded.status, latest.status, latest.frame],
|
||||
cancelledSeek: cancelledSeek.status,
|
||||
cacheBytesBeforeDispose,
|
||||
restartedCacheBytes,
|
||||
disposed,
|
||||
restartedDisposed,
|
||||
manifestSha256,
|
||||
storedSessionSha256: storedSession.sha256,
|
||||
reopenedFrame: reopenedSeek.frame,
|
||||
@@ -230,6 +230,11 @@ test("indexes, seeks, cancels and reopens a bounded one-million-frame media time
|
||||
randomSeekMs: result.randomSeekMs,
|
||||
cacheBytes: result.randomCache?.bytes,
|
||||
evictions: result.randomCache?.evictions,
|
||||
releasedCacheBytes: result.disposed.releasedCacheBytes,
|
||||
cacheBytesAfterDispose: result.disposed.cacheBytesAfter,
|
||||
restartedReleasedCacheBytes: result.restartedDisposed.releasedCacheBytes,
|
||||
restartedCacheBytesAfterDispose: result.restartedDisposed.cacheBytesAfter,
|
||||
workerRestartRecovered: result.reopenedStatus === "COMPLETED",
|
||||
manifestSha256: result.manifestSha256,
|
||||
elapsedMs: result.elapsedMs,
|
||||
}));
|
||||
@@ -264,8 +269,10 @@ test("indexes, seeks, cancels and reopens a bounded one-million-frame media time
|
||||
expect(result.randomCache?.evictions).toBeGreaterThan(0);
|
||||
expect(result.randomCache?.keys).toEqual(expect.arrayContaining(["media:audio:597927", "media:image:1"]));
|
||||
expect(result.randomSeekMs).toBeLessThan(15_000);
|
||||
expect(result.cacheBytesBeforeDispose).toBeLessThanOrEqual(20 * 1024);
|
||||
expect(result.restartedCacheBytes).toBeLessThanOrEqual(20 * 1024);
|
||||
expect(result.disposed.releasedCacheBytes).toBeLessThanOrEqual(20 * 1024);
|
||||
expect(result.disposed.cacheBytesAfter).toBe(0);
|
||||
expect(result.restartedDisposed.releasedCacheBytes).toBeLessThanOrEqual(20 * 1024);
|
||||
expect(result.restartedDisposed.cacheBytesAfter).toBe(0);
|
||||
expect(result.manifestSha256).toBe(result.storedSessionSha256);
|
||||
expect(result.manifestSha256).toBe("d2e7e3c5ed9ae4fda6fe358cebb474f5774f1d2037dff3df9b6dfde9e0bebd2d");
|
||||
expect(result.elapsedMs).toBeLessThan(30_000);
|
||||
|
||||
@@ -54,6 +54,7 @@ test("meets the Chromium OPFS Simulation cache playback performance gate", async
|
||||
const saved = await writer.saveProject(projectId, 1, sourceBlend.slice(0));
|
||||
const stored = await writer.putSimulationCache(projectId, manifest, payload);
|
||||
writer.terminate();
|
||||
const writerPendingAfterTerminate = writer.getPendingRequestCount();
|
||||
const storedAt = performance.now();
|
||||
|
||||
const reader = new StorageClient();
|
||||
@@ -87,6 +88,11 @@ test("meets the Chromium OPFS Simulation cache playback performance gate", async
|
||||
const playbackResult = await playback.play();
|
||||
const finished = performance.now();
|
||||
reader.terminate();
|
||||
const readerPendingAfterTerminate = reader.getPendingRequestCount();
|
||||
const recovery = new StorageClient();
|
||||
const recovered = await recovery.readSimulationCacheFrame(projectId, stored.cacheKey, frameCount);
|
||||
recovery.terminate();
|
||||
const recoveryPendingAfterTerminate = recovery.getPendingRequestCount();
|
||||
return {
|
||||
backend: saved.backend,
|
||||
frameCount,
|
||||
@@ -99,12 +105,18 @@ test("meets the Chromium OPFS Simulation cache playback performance gate", async
|
||||
storeMs: Math.round(storedAt - started),
|
||||
playbackMs: Math.round(finished - storedAt),
|
||||
elapsedMs: Math.round(finished - started),
|
||||
peakCacheBytes: manifest.byteLength,
|
||||
pendingRequestsAfterTerminate: writerPendingAfterTerminate + readerPendingAfterTerminate + recoveryPendingAfterTerminate,
|
||||
workerRestartRecovered: recovered.data.byteLength === frameBytes,
|
||||
};
|
||||
});
|
||||
console.log("simulation-cache-performance", JSON.stringify(result));
|
||||
expect(result.backend).toBe("opfs");
|
||||
expect(result).toMatchObject({ frameCount: 600, byteLength: 52_800, status: "COMPLETED", appliedFrames: 600, lastFrame: 600, publishedFrames: 600 });
|
||||
expect(result.lastTranslation).toBeCloseTo(60, 5);
|
||||
expect(result.storeMs).toBeLessThan(30_000);
|
||||
expect(result.playbackMs).toBeLessThan(30_000);
|
||||
expect(result.elapsedMs).toBeLessThan(30_000);
|
||||
expect(result.pendingRequestsAfterTerminate).toBe(0);
|
||||
expect(result.workerRestartRecovered).toBe(true);
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ test("opens the Blender-style editor and initializes workers", async ({ page })
|
||||
await page.goto("/");
|
||||
appOrigin = new URL(page.url()).origin;
|
||||
|
||||
await expect(page.getByText("Blender Web", { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByText("Web Blender Modeler V1", { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Layout" })).toHaveClass(/active/);
|
||||
await expect(page.locator(".status-bar")).toContainText("Manifest: verified r1");
|
||||
await expect(page.locator(".status-bar")).toContainText("Engine: ready, open a .blend file", { timeout: 20_000 });
|
||||
@@ -2291,7 +2291,7 @@ test("undoes and redoes native scene commands without a Three.js shadow state",
|
||||
expect(result.redoRevision).toBe(4);
|
||||
});
|
||||
|
||||
test("wires Blender Web top-bar undo and redo to native history", async ({ page }) => {
|
||||
test("wires Web Blender Modeler top-bar undo and redo to native history", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.setInputFiles("[data-testid=blend-file-input]", basicBlend);
|
||||
await expect(page.getByText("BasicCube", { exact: true })).toBeVisible();
|
||||
|
||||
@@ -23,15 +23,27 @@ test("decodes, uploads and renders a validated 4K texture in Chromium", async ({
|
||||
const renderer = new WebGLRenderer({ canvas, preserveDrawingBuffer: true }); renderer.setSize(64, 64, false);
|
||||
const scene = new Scene(); const camera = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10); camera.position.z = 1;
|
||||
const material = new MeshBasicMaterial({ map: store.get("image:4k", "BASE_COLOR") });
|
||||
scene.add(new Mesh(new PlaneGeometry(2, 2), material)); renderer.render(scene, camera);
|
||||
const geometry = new PlaneGeometry(2, 2);
|
||||
scene.add(new Mesh(geometry, material)); renderer.render(scene, camera);
|
||||
const gl = renderer.getContext(); const pixels = new Uint8Array(64 * 64 * 4); gl.readPixels(0, 0, 64, 64, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||
let colored = 0; for (let index = 0; index < pixels.length; index += 4) if (pixels[index] > 60 || pixels[index + 1] > 60) colored += 1;
|
||||
const elapsedMs = Math.round(performance.now() - started);
|
||||
material.dispose(); renderer.dispose(); store.dispose(); canvas.remove();
|
||||
return { status, byteLength: data.byteLength, colored, elapsedMs };
|
||||
material.dispose(); geometry.dispose(); renderer.dispose(); store.dispose(); canvas.remove();
|
||||
const released = store.get("image:4k", "BASE_COLOR") === undefined;
|
||||
const recoveryCanvas = document.createElement("canvas"); recoveryCanvas.width = 16; recoveryCanvas.height = 16; document.body.append(recoveryCanvas);
|
||||
const recoveryRenderer = new WebGLRenderer({ canvas: recoveryCanvas, preserveDrawingBuffer: true }); recoveryRenderer.setSize(16, 16, false);
|
||||
const recoveryScene = new Scene(); const recoveryMaterial = new MeshBasicMaterial({ color: 0x33cc66 }); const recoveryGeometry = new PlaneGeometry(2, 2);
|
||||
recoveryScene.add(new Mesh(recoveryGeometry, recoveryMaterial)); recoveryRenderer.render(recoveryScene, camera);
|
||||
const recoveryPixels = new Uint8Array(16 * 16 * 4); recoveryRenderer.getContext().readPixels(0, 0, 16, 16, recoveryRenderer.getContext().RGBA, recoveryRenderer.getContext().UNSIGNED_BYTE, recoveryPixels);
|
||||
const recoveryVisible = recoveryPixels.some((value, index) => index % 4 !== 3 && value > 40);
|
||||
recoveryMaterial.dispose(); recoveryGeometry.dispose(); recoveryRenderer.dispose(); recoveryCanvas.remove();
|
||||
return { width: 4096, height: 4096, status, byteLength: data.byteLength, estimatedPeakGpuBytes: 4096 * 4096 * 4, released, recoveryVisible, colored, elapsedMs };
|
||||
});
|
||||
console.log("texture-4k-performance", JSON.stringify(result));
|
||||
expect(result.status).toMatchObject({ loaded: 1, rejected: 0, bytes: result.byteLength });
|
||||
expect(result.byteLength).toBeGreaterThan(0);
|
||||
expect(result.colored).toBeGreaterThan(3_000);
|
||||
expect(result.elapsedMs).toBeLessThan(30_000);
|
||||
expect(result.released).toBe(true);
|
||||
expect(result.recoveryVisible).toBe(true);
|
||||
});
|
||||
|
||||
@@ -21,16 +21,27 @@ test("decodes, uploads and renders a validated 8K texture in Chromium", async ({
|
||||
const asset = await createGPUTextureAsset({ assetId: "asset:8k", imageId: "image:8k", mimeType: "image/png", width: 8192, height: 8192, usage: "BASE_COLOR", colorSpace: "SRGB" }, data);
|
||||
const started = performance.now(); const store = new GPUTextureStore(); const status = await store.upload([asset]);
|
||||
const scene = new Scene(); const camera = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10); camera.position.z = 1;
|
||||
const material = new MeshBasicMaterial({ map: store.get("image:8k", "BASE_COLOR") }); scene.add(new Mesh(new PlaneGeometry(2, 2), material)); renderer.render(scene, camera);
|
||||
const material = new MeshBasicMaterial({ map: store.get("image:8k", "BASE_COLOR") }); const geometry = new PlaneGeometry(2, 2); scene.add(new Mesh(geometry, material)); renderer.render(scene, camera);
|
||||
const pixels = new Uint8Array(64 * 64 * 4); gl.readPixels(0, 0, 64, 64, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||
let colored = 0; for (let index = 0; index < pixels.length; index += 4) if (pixels[index] > 60 || pixels[index + 1] > 60 || pixels[index + 2] > 60) colored += 1;
|
||||
const elapsedMs = Math.round(performance.now() - started);
|
||||
material.dispose(); renderer.dispose(); store.dispose(); target.remove();
|
||||
return { supported: true, maxTextureSize, status, byteLength: data.byteLength, colored, elapsedMs };
|
||||
material.dispose(); geometry.dispose(); renderer.dispose(); store.dispose(); target.remove();
|
||||
const released = store.get("image:8k", "BASE_COLOR") === undefined;
|
||||
const recoveryCanvas = document.createElement("canvas"); recoveryCanvas.width = 16; recoveryCanvas.height = 16; document.body.append(recoveryCanvas);
|
||||
const recoveryRenderer = new WebGLRenderer({ canvas: recoveryCanvas, preserveDrawingBuffer: true }); recoveryRenderer.setSize(16, 16, false);
|
||||
const recoveryScene = new Scene(); const recoveryMaterial = new MeshBasicMaterial({ color: 0x3366cc }); const recoveryGeometry = new PlaneGeometry(2, 2);
|
||||
recoveryScene.add(new Mesh(recoveryGeometry, recoveryMaterial)); recoveryRenderer.render(recoveryScene, camera);
|
||||
const recoveryPixels = new Uint8Array(16 * 16 * 4); recoveryRenderer.getContext().readPixels(0, 0, 16, 16, recoveryRenderer.getContext().RGBA, recoveryRenderer.getContext().UNSIGNED_BYTE, recoveryPixels);
|
||||
const recoveryVisible = recoveryPixels.some((value, index) => index % 4 !== 3 && value > 40);
|
||||
recoveryMaterial.dispose(); recoveryGeometry.dispose(); recoveryRenderer.dispose(); recoveryCanvas.remove();
|
||||
return { supported: true, maxTextureSize, width: 8192, height: 8192, status, byteLength: data.byteLength, estimatedPeakGpuBytes: 8192 * 8192 * 4, released, recoveryVisible, colored, elapsedMs };
|
||||
});
|
||||
console.log("texture-8k-performance", JSON.stringify(result));
|
||||
expect(result.supported, `WebGL MAX_TEXTURE_SIZE=${result.maxTextureSize}`).toBe(true);
|
||||
expect(result.status).toMatchObject({ loaded: 1, rejected: 0, bytes: result.byteLength });
|
||||
expect(result.byteLength).toBeGreaterThan(0);
|
||||
expect(result.colored).toBeGreaterThan(3_000);
|
||||
expect(result.elapsedMs).toBeLessThan(45_000);
|
||||
expect(result.released).toBe(true);
|
||||
expect(result.recoveryVisible).toBe(true);
|
||||
});
|
||||
|
||||
41
web/tests/e2e/wasm-threading-capability.spec.ts
Normal file
41
web/tests/e2e/wasm-threading-capability.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("gates only the pthread WASM engine on its three platform requirements", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const gates = await page.evaluate(async () => {
|
||||
const { gateWasmThreadingCapability } = await import("/src/platform/capabilities.ts");
|
||||
return {
|
||||
ready: gateWasmThreadingCapability({ crossOriginIsolated: true, sharedArrayBuffer: true, worker: true }),
|
||||
noIsolation: gateWasmThreadingCapability({ crossOriginIsolated: false, sharedArrayBuffer: true, worker: true }),
|
||||
noSharedArrayBuffer: gateWasmThreadingCapability({ crossOriginIsolated: true, sharedArrayBuffer: false, worker: true }),
|
||||
noWorker: gateWasmThreadingCapability({ crossOriginIsolated: true, sharedArrayBuffer: true, worker: false }),
|
||||
none: gateWasmThreadingCapability({ crossOriginIsolated: false, sharedArrayBuffer: false, worker: false }),
|
||||
};
|
||||
});
|
||||
|
||||
expect(gates.ready).toEqual({
|
||||
taskId: "M6-02",
|
||||
capability: "WASM_PTHREAD_ENGINE",
|
||||
status: "READY",
|
||||
issues: [],
|
||||
});
|
||||
expect(gates.noIsolation.issues.map(({ code, path }) => ({ code, path }))).toEqual([
|
||||
{ code: "PLATFORM_CAPABILITY_UNAVAILABLE", path: "crossOriginIsolated" },
|
||||
]);
|
||||
expect(gates.noSharedArrayBuffer.issues.map(({ code, path }) => ({ code, path }))).toEqual([
|
||||
{ code: "PLATFORM_CAPABILITY_UNAVAILABLE", path: "sharedArrayBuffer" },
|
||||
]);
|
||||
expect(gates.noWorker.issues.map(({ code, path }) => ({ code, path }))).toEqual([
|
||||
{ code: "PLATFORM_CAPABILITY_UNAVAILABLE", path: "worker" },
|
||||
]);
|
||||
expect(gates.none).toMatchObject({
|
||||
taskId: "M6-02",
|
||||
capability: "WASM_PTHREAD_ENGINE",
|
||||
status: "BLOCKED",
|
||||
});
|
||||
expect(gates.none.issues.map(({ code, path }) => ({ code, path }))).toEqual([
|
||||
{ code: "PLATFORM_CAPABILITY_UNAVAILABLE", path: "crossOriginIsolated" },
|
||||
{ code: "PLATFORM_CAPABILITY_UNAVAILABLE", path: "sharedArrayBuffer" },
|
||||
{ code: "PLATFORM_CAPABILITY_UNAVAILABLE", path: "worker" },
|
||||
]);
|
||||
});
|
||||
44
web/tests/unit/v1-acceptance.test.mjs
Normal file
44
web/tests/unit/v1-acceptance.test.mjs
Normal file
@@ -0,0 +1,44 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { buildAcceptancePlan, readAcceptanceInputs } from "../../../tools/web/v1-acceptance-lib.mjs";
|
||||
|
||||
const inputs = readAcceptanceInputs();
|
||||
|
||||
function clonedInputs() {
|
||||
return {
|
||||
ledger: structuredClone(inputs.ledger),
|
||||
packageManifest: structuredClone(inputs.packageManifest),
|
||||
};
|
||||
}
|
||||
|
||||
test("V1 acceptance declarations resolve to executable Web commands", () => {
|
||||
const plan = buildAcceptancePlan(inputs.ledger, inputs.packageManifest);
|
||||
assert.deepEqual(
|
||||
{ families: plan.familyCount, declarations: plan.declarationCount, unique: plan.uniqueCount },
|
||||
{ families: 12, declarations: 50, unique: 49 },
|
||||
);
|
||||
});
|
||||
|
||||
test("V1 acceptance rejects an unknown namespace", () => {
|
||||
const { ledger, packageManifest } = clonedInputs();
|
||||
ledger.families[0].acceptance[0] = "desktop:test:unknown";
|
||||
assert.throws(() => buildAcceptancePlan(ledger, packageManifest), /unsupported acceptance namespace/);
|
||||
});
|
||||
|
||||
test("V1 acceptance rejects a missing package script", () => {
|
||||
const { ledger, packageManifest } = clonedInputs();
|
||||
delete packageManifest.scripts["test:nonmesh-roundtrip"];
|
||||
assert.throws(() => buildAcceptancePlan(ledger, packageManifest), /package script test:nonmesh-roundtrip is missing/);
|
||||
});
|
||||
|
||||
test("V1 acceptance rejects an empty E2E pattern", () => {
|
||||
const { ledger, packageManifest } = clonedInputs();
|
||||
ledger.families[0].acceptance[0] = "web:e2e: ";
|
||||
assert.throws(() => buildAcceptancePlan(ledger, packageManifest), /E2E pattern must not be empty/);
|
||||
});
|
||||
|
||||
test("V1 acceptance rejects duplicate declarations within one family", () => {
|
||||
const { ledger, packageManifest } = clonedInputs();
|
||||
ledger.families[0].acceptance.push(ledger.families[0].acceptance[0]);
|
||||
assert.throws(() => buildAcceptancePlan(ledger, packageManifest), /duplicate acceptance token/);
|
||||
});
|
||||
Reference in New Issue
Block a user