added optional web component UI control
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user