added optional web component UI control
All checks were successful
CI / React UI Build (push) Successful in 12s
CI / Native Windows Build And Tests (push) Successful in 2m20s
CI / Windows Release Package (push) Successful in 2m56s

This commit is contained in:
Aiden
2026-05-30 22:57:59 +10:00
parent a6d2ee385e
commit 27690c3afa
26 changed files with 804 additions and 76 deletions

View File

@@ -157,6 +157,53 @@ void TestRootServesUiIndex()
std::filesystem::remove_all(root);
}
void TestShaderAssetEndpointUsesCallback()
{
using namespace RenderCadenceCompositor;
const std::filesystem::path root = std::filesystem::temp_directory_path() / "render-cadence-compositor-shader-asset-test";
std::filesystem::create_directories(root / "ui");
const std::filesystem::path assetPath = root / "ui" / "controls.js";
{
std::ofstream output(assetPath, std::ios::binary);
output << "customElements.define('solid-controls', class extends HTMLElement {});";
}
HttpControlServer server;
RenderCadenceHttpRouteCallbacks callbacks;
callbacks.resolveShaderAssetPath = [&assetPath](const std::string& shaderId, const std::string& relativePath, std::filesystem::path& resolvedPath, std::string&) {
ExpectEquals(shaderId, "solid", "shader asset callback receives shader id");
ExpectEquals(relativePath, "ui/controls.js", "shader asset callback receives package-relative asset path");
resolvedPath = assetPath;
return true;
};
HttpControlServer::HttpRequest request;
request.method = "GET";
request.path = "/shader-assets/solid/ui/controls.js";
const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks);
ExpectEquals(response.status, "200 OK", "shader asset endpoint serves resolved asset");
ExpectEquals(response.contentType, "text/javascript", "shader asset endpoint guesses javascript content type");
Expect(response.body.find("solid-controls") != std::string::npos, "shader asset endpoint returns asset body");
std::filesystem::remove_all(root);
}
void TestIncompleteShaderAssetEndpointReturns404()
{
using namespace RenderCadenceCompositor;
HttpControlServer server;
HttpControlServer::HttpRequest request;
request.method = "GET";
request.path = "/shader-assets/solid";
RenderCadenceHttpRouteCallbacks callbacks;
const HttpControlServer::HttpResponse response = RouteRenderCadenceHttpRequest(request, server, callbacks);
ExpectEquals(response.status, "404 Not Found", "incomplete shader asset endpoint returns 404");
}
void TestKnownPostEndpointReturnsActionError()
{
using namespace RenderCadenceCompositor;
@@ -285,6 +332,8 @@ int main()
TestNdiSourcesEndpointUsesCallback();
TestWebSocketAcceptKey();
TestRootServesUiIndex();
TestShaderAssetEndpointUsesCallback();
TestIncompleteShaderAssetEndpointReturns404();
TestKnownPostEndpointReturnsActionError();
TestLayerPostEndpointsUseCallbacks();
TestGenericPostCallbackHandlesControlRoutes();

View File

@@ -66,12 +66,14 @@ int main()
const std::filesystem::path root = MakeTestRoot();
WriteFile(root / "solid-color" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
WriteFile(root / "solid-color" / "ui" / "controls.js", "customElements.define('solid-color-controls', class extends HTMLElement {});\n");
WriteFile(root / "solid-color" / "shader.json", R"({
"id": "solid-color",
"name": "Solid Color",
"description": "A single color shader.",
"category": "Generator",
"entryPoint": "shadeVideo",
"ui": { "type": "webComponent", "entry": "ui/controls.js", "tag": "solid-color-controls" },
"parameters": [
{
"id": "color",
@@ -120,6 +122,7 @@ int main()
const std::string json = RenderCadenceCompositor::RuntimeStateToJson(stateInput);
ExpectContains(json, "\"shaders\":[{\"id\":\"solid-color\"", "state JSON should include supported shaders");
ExpectContains(json, "\"ui\":{\"type\":\"webComponent\",\"entry\":\"ui/controls.js\",\"tag\":\"solid-color-controls\",\"assetUrl\":\"/shader-assets/solid-color/ui/controls.js\"}", "state JSON should expose shader custom UI metadata");
ExpectContains(json, "\"layerCount\":1", "state JSON should expose the display layer count");
ExpectContains(json, "\"layers\":[{\"id\":\"runtime-layer-1\"", "state JSON should expose the active display layer");
ExpectContains(json, "\"parameters\":[{\"id\":\"color\"", "state JSON should expose active shader parameters");

View File

@@ -50,12 +50,14 @@ void TestValidManifest()
WriteFile(root / "look" / "mask.png", "not a real png, but enough for existence checks");
WriteFile(root / "look" / "Inter.ttf", "not a real font, but enough for existence checks");
WriteFile(root / "look" / "Mono.ttf", "not a real font, but enough for existence checks");
WriteFile(root / "look" / "ui" / "controls.js", "customElements.define('look-controls', class extends HTMLElement {});\n");
WriteShaderPackage(root, "look", R"({
"id": "look-01",
"name": "Look 01",
"description": "Test package",
"category": "Tests",
"entryPoint": "shadeVideo",
"ui": { "type": "webComponent", "entry": "ui/controls.js", "tag": "look-controls" },
"textures": [{ "id": "maskTex", "path": "mask.png" }],
"fonts": [
{ "id": "inter", "path": "Inter.ttf" },
@@ -83,6 +85,8 @@ void TestValidManifest()
Expect(package.fontAssets.size() == 2 && package.fontAssets[0].id == "inter", "font assets parse");
Expect(package.temporal.enabled && package.temporal.effectiveHistoryLength == 4, "temporal history is capped");
Expect(package.feedback.enabled && package.feedback.writePassId == "main", "feedback defaults to the implicit main pass");
Expect(package.ui.enabled && package.ui.entryPath == "ui/controls.js", "custom UI entry parses");
Expect(package.ui.customElementTag == "look-controls", "custom UI tag parses");
Expect(package.parameters.size() == 4, "parameters parse");
Expect(package.parameters[0].description == "Scales the output intensity.", "parameter descriptions parse");
Expect(package.parameters[1].type == ShaderParameterType::Text && package.parameters[1].defaultTextValue == "LIVE", "text parameter parses");
@@ -93,6 +97,47 @@ void TestValidManifest()
std::filesystem::remove_all(root);
}
void TestInvalidUiDefinition()
{
struct Case
{
const char* directoryName;
const char* uiJson;
const char* expectedError;
bool writeEntry;
};
const Case cases[] = {
{ "bad-type", R"({ "type": "react", "entry": "ui/controls.js", "tag": "bad-type-controls" })", "webComponent", true },
{ "bad-path", R"({ "type": "webComponent", "entry": "../controls.js", "tag": "bad-path-controls" })", "safe relative", true },
{ "wrong-dir", R"({ "type": "webComponent", "entry": "controls.js", "tag": "wrong-dir-controls" })", "safe relative", true },
{ "bad-extension", R"({ "type": "webComponent", "entry": "ui/controls.txt", "tag": "bad-extension-controls" })", ".js or .mjs", true },
{ "bad-tag", R"({ "type": "webComponent", "entry": "ui/controls.js", "tag": "BadTag" })", "custom element", true },
{ "missing-entry", R"({ "type": "webComponent", "entry": "ui/missing.js", "tag": "missing-entry-controls" })", "UI entry not found", false },
};
const std::filesystem::path root = MakeTestRoot();
for (const Case& testCase : cases)
{
WriteShaderPackage(root, testCase.directoryName, std::string(R"({
"id": ")") + testCase.directoryName + R"(",
"name": "Bad UI",
"ui": )" + testCase.uiJson + R"(,
"parameters": []
})");
if (testCase.writeEntry)
WriteFile(root / testCase.directoryName / "ui" / "controls.js", "customElements.define('bad-controls', class extends HTMLElement {});\n");
ShaderPackageRegistry registry(4);
ShaderPackage package;
std::string error;
Expect(!registry.ParseManifest(root / testCase.directoryName / "shader.json", package, error), "invalid custom UI manifest is rejected");
Expect(error.find(testCase.expectedError) != std::string::npos, "invalid custom UI error explains the rejected field");
}
std::filesystem::remove_all(root);
}
void TestExplicitPassManifest()
{
const std::filesystem::path root = MakeTestRoot();
@@ -289,6 +334,7 @@ int main()
{
TestValidManifest();
TestExplicitPassManifest();
TestInvalidUiDefinition();
TestMissingFontAsset();
TestInvalidManifest();
TestInvalidTemporalSettings();