From acfe67626efdce8a4866841ffda61af5da9c43d5 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 27 Jan 2026 11:16:03 -0400 Subject: [PATCH] Show a tab of dependents in the HTML explorer Signed-off-by: Juan Cruz Viotti --- src/index/index.cc | 1 + src/web/pages/schema.cc | 97 +++++++++++++ test/e2e/populated/api/list.hurl | 2 +- .../populated/api/schemas-dependencies.hurl | 53 ++++++++ .../e2e/populated/api/schemas-dependents.hurl | 50 +++++++ test/sandbox/manifest-headless.txt | 117 ++++++++++++++++ test/sandbox/manifest-html.txt | 127 ++++++++++++++++++ .../schemas/test/folder/cycle/bar.json | 6 + .../schemas/test/folder/cycle/baz.json | 6 + .../schemas/test/folder/cycle/foo.json | 6 + .../schemas/test/folder/recursive.json | 6 + test/ui/dependencies.spec.js | 123 +++++++++++++++++ 12 files changed, 593 insertions(+), 1 deletion(-) create mode 100644 test/sandbox/schemas/test/folder/cycle/bar.json create mode 100644 test/sandbox/schemas/test/folder/cycle/baz.json create mode 100644 test/sandbox/schemas/test/folder/cycle/foo.json create mode 100644 test/sandbox/schemas/test/folder/recursive.json create mode 100644 test/ui/dependencies.spec.js diff --git a/src/index/index.cc b/src/index/index.cc index 0654399d..7f6eb9ea 100644 --- a/src/index/index.cc +++ b/src/index/index.cc @@ -494,6 +494,7 @@ static auto index_main(const std::string_view &program, entry.parent_path() / "schema-html.metapack", {entry, schema_path / "dependencies.metapack", schema_path / "health.metapack", + schema_path / "dependents.metapack", // We rely on the configuration for site metadata mark_configuration_path, mark_version_path}, configuration, mutex, "Rendering", relative_path.string(), diff --git a/src/web/pages/schema.cc b/src/web/pages/schema.cc index cd350888..54b6d40f 100644 --- a/src/web/pages/schema.cc +++ b/src/web/pages/schema.cc @@ -10,6 +10,7 @@ #include // std::chrono #include // std::filesystem #include // std::reference_wrapper +#include // std::set #include // std::ostringstream #include // std::vector @@ -122,6 +123,27 @@ auto GENERATE_WEB_SCHEMA::handler( const auto health{read_json(dependencies.at(2))}; assert(health.is_object()); assert(health.defines("errors")); + const auto dependents_json{read_json(dependencies.at(3))}; + assert(dependents_json.is_array()); + + // Collect unique dependent schemas, preferring direct over indirect. + // The dependency tree already takes care of self-references, so + // a schema should never appear as its own dependent + std::set direct_dependent_schemas; + std::set indirect_dependent_schemas; + for (const auto &dependent : dependents_json.as_array()) { + assert(dependent.at("from") != dependent.at("to")); + if (dependent.at("to") == meta.at("identifier")) { + direct_dependent_schemas.emplace(dependent.at("from").to_string()); + } else { + indirect_dependent_schemas.emplace(dependent.at("from").to_string()); + } + } + + // If a schema is a direct dependent, don't also show it as indirect + for (const auto &schema : direct_dependent_schemas) { + indirect_dependent_schemas.erase(schema); + } // Tab navigation std::vector nav_items; @@ -147,6 +169,18 @@ auto GENERATE_WEB_SCHEMA::handler( span({{"class", "ms-2 badge rounded-pill text-bg-secondary align-text-top"}}, std::to_string(dependencies_json.size()))))); + nav_items.emplace_back(li( + {{"class", "nav-item"}}, + button( + {{"class", "nav-link"}, + {"type", "button"}, + {"role", "tab"}, + {"data-sourcemeta-ui-tab-target", "dependents"}}, + span("Dependents"), + span({{"class", + "ms-2 badge rounded-pill text-bg-secondary align-text-top"}}, + std::to_string(direct_dependent_schemas.size() + + indirect_dependent_schemas.size()))))); nav_items.emplace_back(li( {{"class", "nav-item"}}, button( @@ -248,6 +282,69 @@ auto GENERATE_WEB_SCHEMA::handler( div({{"data-sourcemeta-ui-tab-id", "dependencies"}, {"class", "d-none"}}, dependencies_content)); + // Dependents tab + std::ostringstream dependent_summary; + dependent_summary + << "This schema has " << direct_dependent_schemas.size() << " direct " + << (direct_dependent_schemas.size() == 1 ? "dependent" : "dependents") + << " and " << indirect_dependent_schemas.size() << " indirect " + << (indirect_dependent_schemas.size() == 1 ? "dependent" : "dependents") + << "."; + + std::vector dependents_content; + dependents_content.emplace_back(p(dependent_summary.str())); + + if (!direct_dependent_schemas.empty() || + !indirect_dependent_schemas.empty()) { + std::vector dep_tab_rows; + + const auto render_dependent_row{ + [&dep_tab_rows, + &configuration](const sourcemeta::core::JSON::String &schema, + const bool is_direct) -> void { + std::vector row_cells; + if (is_direct) { + row_cells.emplace_back( + td(span({{"class", "badge text-bg-primary"}}, "Direct"))); + } else { + row_cells.emplace_back( + td(span({{"class", "badge text-bg-dark"}}, "Indirect"))); + } + + if (schema.starts_with(configuration.url)) { + std::filesystem::path dependent_schema_url{ + schema.substr(configuration.url.size())}; + if (dependent_schema_url.extension() == ".json") { + dependent_schema_url.replace_extension(""); + } + row_cells.emplace_back( + td(code(a({{"href", dependent_schema_url.string()}}, + dependent_schema_url.string())))); + } else { + row_cells.emplace_back(td(code(schema))); + } + + dep_tab_rows.emplace_back(tr(row_cells)); + }}; + + for (const auto &schema : direct_dependent_schemas) { + render_dependent_row(schema, true); + } + + for (const auto &schema : indirect_dependent_schemas) { + render_dependent_row(schema, false); + } + + dependents_content.emplace_back( + table({{"class", "table table-bordered"}}, + thead(tr(th({{"scope", "col"}}, "Type"), + th({{"scope", "col"}}, "Dependent"))), + tbody(dep_tab_rows))); + } + container_children.emplace_back( + div({{"data-sourcemeta-ui-tab-id", "dependents"}, {"class", "d-none"}}, + dependents_content)); + // Health tab const auto errors_count{health.at("errors").size()}; std::vector health_content; diff --git a/test/e2e/populated/api/list.hurl b/test/e2e/populated/api/list.hurl index a372b037..41f5c096 100644 --- a/test/e2e/populated/api/list.hurl +++ b/test/e2e/populated/api/list.hurl @@ -60,7 +60,7 @@ jsonpath "$.entries[0].health" == 0 jsonpath "$.entries[0].path" == "/test/v2.0/" jsonpath "$.entries[1].name" == "schemas" jsonpath "$.entries[1].type" == "directory" -jsonpath "$.entries[1].health" == 3 +jsonpath "$.entries[1].health" == 7 jsonpath "$.entries[1].path" == "/test/schemas/" jsonpath "$.entries[2].name" == "same" jsonpath "$.entries[2].type" == "directory" diff --git a/test/e2e/populated/api/schemas-dependencies.hurl b/test/e2e/populated/api/schemas-dependencies.hurl index eeedec40..de563b79 100644 --- a/test/e2e/populated/api/schemas-dependencies.hurl +++ b/test/e2e/populated/api/schemas-dependencies.hurl @@ -70,6 +70,59 @@ Link: ; rel="describedby" [Asserts] jsonpath "$.valid" == true +GET {{base}}/self/v1/api/schemas/dependencies/test/schemas/recursive +HTTP 200 +Content-Type: application/json +Access-Control-Allow-Origin: * +Link: ; rel="describedby" +[Captures] +last_response: body +schema_path: header "Link" regex "<([^>]+)>" +[Asserts] +header "ETag" exists +header "Last-Modified" exists +jsonpath "$" count == 0 + +POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}} +``` +{{last_response}} +``` +HTTP 200 +Link: ; rel="describedby" +[Asserts] +jsonpath "$.valid" == true + +GET {{base}}/self/v1/api/schemas/dependencies/test/schemas/cycle/foo +HTTP 200 +Content-Type: application/json +Access-Control-Allow-Origin: * +Link: ; rel="describedby" +[Captures] +last_response: body +schema_path: header "Link" regex "<([^>]+)>" +[Asserts] +header "ETag" exists +header "Last-Modified" exists +jsonpath "$" count == 3 +jsonpath "$[0].from" == "{{base}}/test/schemas/cycle/foo" +jsonpath "$[0].to" == "{{base}}/test/schemas/cycle/bar" +jsonpath "$[0].at" == "/additionalProperties/$ref" +jsonpath "$[1].from" == "{{base}}/test/schemas/cycle/bar" +jsonpath "$[1].to" == "{{base}}/test/schemas/cycle/baz" +jsonpath "$[1].at" == "/additionalProperties/$ref" +jsonpath "$[2].from" == "{{base}}/test/schemas/cycle/baz" +jsonpath "$[2].to" == "{{base}}/test/schemas/cycle/foo" +jsonpath "$[2].at" == "/additionalProperties/$ref" + +POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}} +``` +{{last_response}} +``` +HTTP 200 +Link: ; rel="describedby" +[Asserts] +jsonpath "$.valid" == true + HEAD {{base}}/self/v1/api/schemas/dependencies/test/v2.0/schema HTTP 200 Content-Type: application/json diff --git a/test/e2e/populated/api/schemas-dependents.hurl b/test/e2e/populated/api/schemas-dependents.hurl index 6f61e560..a12395d2 100644 --- a/test/e2e/populated/api/schemas-dependents.hurl +++ b/test/e2e/populated/api/schemas-dependents.hurl @@ -78,6 +78,56 @@ Link: ; rel="describedby" [Asserts] jsonpath "$.valid" == true +GET {{base}}/self/v1/api/schemas/dependents/test/schemas/recursive +HTTP 200 +Content-Type: application/json +Access-Control-Allow-Origin: * +Link: ; rel="describedby" +[Captures] +last_response: body +schema_path: header "Link" regex "<([^>]+)>" +[Asserts] +header "ETag" exists +header "Last-Modified" exists +jsonpath "$" count == 0 + +POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}} +``` +{{last_response}} +``` +HTTP 200 +Link: ; rel="describedby" +[Asserts] +jsonpath "$.valid" == true + +GET {{base}}/self/v1/api/schemas/dependents/test/schemas/cycle/foo +HTTP 200 +Content-Type: application/json +Access-Control-Allow-Origin: * +Link: ; rel="describedby" +[Captures] +last_response: body +schema_path: header "Link" regex "<([^>]+)>" +[Asserts] +header "ETag" exists +header "Last-Modified" exists +jsonpath "$" count == 3 +jsonpath "$[0].from" == "{{base}}/test/schemas/cycle/bar" +jsonpath "$[0].to" == "{{base}}/test/schemas/cycle/baz" +jsonpath "$[1].from" == "{{base}}/test/schemas/cycle/baz" +jsonpath "$[1].to" == "{{base}}/test/schemas/cycle/foo" +jsonpath "$[2].from" == "{{base}}/test/schemas/cycle/foo" +jsonpath "$[2].to" == "{{base}}/test/schemas/cycle/bar" + +POST {{base}}/self/v1/api/schemas/evaluate{{schema_path}} +``` +{{last_response}} +``` +HTTP 200 +Link: ; rel="describedby" +[Asserts] +jsonpath "$.valid" == true + HEAD {{base}}/self/v1/api/schemas/dependents/test/bundling/double HTTP 200 Content-Type: application/json diff --git a/test/sandbox/manifest-headless.txt b/test/sandbox/manifest-headless.txt index 02db474c..dd17b23d 100644 --- a/test/sandbox/manifest-headless.txt +++ b/test/sandbox/manifest-headless.txt @@ -373,6 +373,22 @@ ./explorer/test/schemas/clash/foo/bar/% ./explorer/test/schemas/clash/foo/bar/%/schema.metapack ./explorer/test/schemas/clash/foo/bar/%/schema.metapack.deps +./explorer/test/schemas/cycle +./explorer/test/schemas/cycle/% +./explorer/test/schemas/cycle/%/directory.metapack +./explorer/test/schemas/cycle/%/directory.metapack.deps +./explorer/test/schemas/cycle/bar +./explorer/test/schemas/cycle/bar/% +./explorer/test/schemas/cycle/bar/%/schema.metapack +./explorer/test/schemas/cycle/bar/%/schema.metapack.deps +./explorer/test/schemas/cycle/baz +./explorer/test/schemas/cycle/baz/% +./explorer/test/schemas/cycle/baz/%/schema.metapack +./explorer/test/schemas/cycle/baz/%/schema.metapack.deps +./explorer/test/schemas/cycle/foo +./explorer/test/schemas/cycle/foo/% +./explorer/test/schemas/cycle/foo/%/schema.metapack +./explorer/test/schemas/cycle/foo/%/schema.metapack.deps ./explorer/test/schemas/html ./explorer/test/schemas/html/% ./explorer/test/schemas/html/%/schema.metapack @@ -405,6 +421,10 @@ ./explorer/test/schemas/no-schema/% ./explorer/test/schemas/no-schema/%/schema.metapack ./explorer/test/schemas/no-schema/%/schema.metapack.deps +./explorer/test/schemas/recursive +./explorer/test/schemas/recursive/% +./explorer/test/schemas/recursive/%/schema.metapack +./explorer/test/schemas/recursive/%/schema.metapack.deps ./explorer/test/schemas/string ./explorer/test/schemas/string/% ./explorer/test/schemas/string/%/directory.metapack @@ -1710,6 +1730,79 @@ ./schemas/test/schemas/clash/foo/bar/%/schema.metapack.deps ./schemas/test/schemas/clash/foo/bar/%/stats.metapack ./schemas/test/schemas/clash/foo/bar/%/stats.metapack.deps +./schemas/test/schemas/cycle +./schemas/test/schemas/cycle/bar +./schemas/test/schemas/cycle/bar/% +./schemas/test/schemas/cycle/bar/%/blaze-exhaustive.metapack +./schemas/test/schemas/cycle/bar/%/blaze-exhaustive.metapack.deps +./schemas/test/schemas/cycle/bar/%/blaze-fast.metapack +./schemas/test/schemas/cycle/bar/%/blaze-fast.metapack.deps +./schemas/test/schemas/cycle/bar/%/bundle.metapack +./schemas/test/schemas/cycle/bar/%/bundle.metapack.deps +./schemas/test/schemas/cycle/bar/%/dependencies.metapack +./schemas/test/schemas/cycle/bar/%/dependencies.metapack.deps +./schemas/test/schemas/cycle/bar/%/dependents.metapack +./schemas/test/schemas/cycle/bar/%/dependents.metapack.deps +./schemas/test/schemas/cycle/bar/%/editor.metapack +./schemas/test/schemas/cycle/bar/%/editor.metapack.deps +./schemas/test/schemas/cycle/bar/%/health.metapack +./schemas/test/schemas/cycle/bar/%/health.metapack.deps +./schemas/test/schemas/cycle/bar/%/locations.metapack +./schemas/test/schemas/cycle/bar/%/locations.metapack.deps +./schemas/test/schemas/cycle/bar/%/positions.metapack +./schemas/test/schemas/cycle/bar/%/positions.metapack.deps +./schemas/test/schemas/cycle/bar/%/schema.metapack +./schemas/test/schemas/cycle/bar/%/schema.metapack.deps +./schemas/test/schemas/cycle/bar/%/stats.metapack +./schemas/test/schemas/cycle/bar/%/stats.metapack.deps +./schemas/test/schemas/cycle/baz +./schemas/test/schemas/cycle/baz/% +./schemas/test/schemas/cycle/baz/%/blaze-exhaustive.metapack +./schemas/test/schemas/cycle/baz/%/blaze-exhaustive.metapack.deps +./schemas/test/schemas/cycle/baz/%/blaze-fast.metapack +./schemas/test/schemas/cycle/baz/%/blaze-fast.metapack.deps +./schemas/test/schemas/cycle/baz/%/bundle.metapack +./schemas/test/schemas/cycle/baz/%/bundle.metapack.deps +./schemas/test/schemas/cycle/baz/%/dependencies.metapack +./schemas/test/schemas/cycle/baz/%/dependencies.metapack.deps +./schemas/test/schemas/cycle/baz/%/dependents.metapack +./schemas/test/schemas/cycle/baz/%/dependents.metapack.deps +./schemas/test/schemas/cycle/baz/%/editor.metapack +./schemas/test/schemas/cycle/baz/%/editor.metapack.deps +./schemas/test/schemas/cycle/baz/%/health.metapack +./schemas/test/schemas/cycle/baz/%/health.metapack.deps +./schemas/test/schemas/cycle/baz/%/locations.metapack +./schemas/test/schemas/cycle/baz/%/locations.metapack.deps +./schemas/test/schemas/cycle/baz/%/positions.metapack +./schemas/test/schemas/cycle/baz/%/positions.metapack.deps +./schemas/test/schemas/cycle/baz/%/schema.metapack +./schemas/test/schemas/cycle/baz/%/schema.metapack.deps +./schemas/test/schemas/cycle/baz/%/stats.metapack +./schemas/test/schemas/cycle/baz/%/stats.metapack.deps +./schemas/test/schemas/cycle/foo +./schemas/test/schemas/cycle/foo/% +./schemas/test/schemas/cycle/foo/%/blaze-exhaustive.metapack +./schemas/test/schemas/cycle/foo/%/blaze-exhaustive.metapack.deps +./schemas/test/schemas/cycle/foo/%/blaze-fast.metapack +./schemas/test/schemas/cycle/foo/%/blaze-fast.metapack.deps +./schemas/test/schemas/cycle/foo/%/bundle.metapack +./schemas/test/schemas/cycle/foo/%/bundle.metapack.deps +./schemas/test/schemas/cycle/foo/%/dependencies.metapack +./schemas/test/schemas/cycle/foo/%/dependencies.metapack.deps +./schemas/test/schemas/cycle/foo/%/dependents.metapack +./schemas/test/schemas/cycle/foo/%/dependents.metapack.deps +./schemas/test/schemas/cycle/foo/%/editor.metapack +./schemas/test/schemas/cycle/foo/%/editor.metapack.deps +./schemas/test/schemas/cycle/foo/%/health.metapack +./schemas/test/schemas/cycle/foo/%/health.metapack.deps +./schemas/test/schemas/cycle/foo/%/locations.metapack +./schemas/test/schemas/cycle/foo/%/locations.metapack.deps +./schemas/test/schemas/cycle/foo/%/positions.metapack +./schemas/test/schemas/cycle/foo/%/positions.metapack.deps +./schemas/test/schemas/cycle/foo/%/schema.metapack +./schemas/test/schemas/cycle/foo/%/schema.metapack.deps +./schemas/test/schemas/cycle/foo/%/stats.metapack +./schemas/test/schemas/cycle/foo/%/stats.metapack.deps ./schemas/test/schemas/html ./schemas/test/schemas/html/% ./schemas/test/schemas/html/%/blaze-exhaustive.metapack @@ -1902,6 +1995,30 @@ ./schemas/test/schemas/no-schema/%/schema.metapack.deps ./schemas/test/schemas/no-schema/%/stats.metapack ./schemas/test/schemas/no-schema/%/stats.metapack.deps +./schemas/test/schemas/recursive +./schemas/test/schemas/recursive/% +./schemas/test/schemas/recursive/%/blaze-exhaustive.metapack +./schemas/test/schemas/recursive/%/blaze-exhaustive.metapack.deps +./schemas/test/schemas/recursive/%/blaze-fast.metapack +./schemas/test/schemas/recursive/%/blaze-fast.metapack.deps +./schemas/test/schemas/recursive/%/bundle.metapack +./schemas/test/schemas/recursive/%/bundle.metapack.deps +./schemas/test/schemas/recursive/%/dependencies.metapack +./schemas/test/schemas/recursive/%/dependencies.metapack.deps +./schemas/test/schemas/recursive/%/dependents.metapack +./schemas/test/schemas/recursive/%/dependents.metapack.deps +./schemas/test/schemas/recursive/%/editor.metapack +./schemas/test/schemas/recursive/%/editor.metapack.deps +./schemas/test/schemas/recursive/%/health.metapack +./schemas/test/schemas/recursive/%/health.metapack.deps +./schemas/test/schemas/recursive/%/locations.metapack +./schemas/test/schemas/recursive/%/locations.metapack.deps +./schemas/test/schemas/recursive/%/positions.metapack +./schemas/test/schemas/recursive/%/positions.metapack.deps +./schemas/test/schemas/recursive/%/schema.metapack +./schemas/test/schemas/recursive/%/schema.metapack.deps +./schemas/test/schemas/recursive/%/stats.metapack +./schemas/test/schemas/recursive/%/stats.metapack.deps ./schemas/test/schemas/string ./schemas/test/schemas/string/% ./schemas/test/schemas/string/%/blaze-exhaustive.metapack diff --git a/test/sandbox/manifest-html.txt b/test/sandbox/manifest-html.txt index d09822c3..f2c7e187 100644 --- a/test/sandbox/manifest-html.txt +++ b/test/sandbox/manifest-html.txt @@ -561,6 +561,30 @@ ./explorer/test/schemas/clash/foo/bar/%/schema-html.metapack.deps ./explorer/test/schemas/clash/foo/bar/%/schema.metapack ./explorer/test/schemas/clash/foo/bar/%/schema.metapack.deps +./explorer/test/schemas/cycle +./explorer/test/schemas/cycle/% +./explorer/test/schemas/cycle/%/directory-html.metapack +./explorer/test/schemas/cycle/%/directory-html.metapack.deps +./explorer/test/schemas/cycle/%/directory.metapack +./explorer/test/schemas/cycle/%/directory.metapack.deps +./explorer/test/schemas/cycle/bar +./explorer/test/schemas/cycle/bar/% +./explorer/test/schemas/cycle/bar/%/schema-html.metapack +./explorer/test/schemas/cycle/bar/%/schema-html.metapack.deps +./explorer/test/schemas/cycle/bar/%/schema.metapack +./explorer/test/schemas/cycle/bar/%/schema.metapack.deps +./explorer/test/schemas/cycle/baz +./explorer/test/schemas/cycle/baz/% +./explorer/test/schemas/cycle/baz/%/schema-html.metapack +./explorer/test/schemas/cycle/baz/%/schema-html.metapack.deps +./explorer/test/schemas/cycle/baz/%/schema.metapack +./explorer/test/schemas/cycle/baz/%/schema.metapack.deps +./explorer/test/schemas/cycle/foo +./explorer/test/schemas/cycle/foo/% +./explorer/test/schemas/cycle/foo/%/schema-html.metapack +./explorer/test/schemas/cycle/foo/%/schema-html.metapack.deps +./explorer/test/schemas/cycle/foo/%/schema.metapack +./explorer/test/schemas/cycle/foo/%/schema.metapack.deps ./explorer/test/schemas/html ./explorer/test/schemas/html/% ./explorer/test/schemas/html/%/schema-html.metapack @@ -609,6 +633,12 @@ ./explorer/test/schemas/no-schema/%/schema-html.metapack.deps ./explorer/test/schemas/no-schema/%/schema.metapack ./explorer/test/schemas/no-schema/%/schema.metapack.deps +./explorer/test/schemas/recursive +./explorer/test/schemas/recursive/% +./explorer/test/schemas/recursive/%/schema-html.metapack +./explorer/test/schemas/recursive/%/schema-html.metapack.deps +./explorer/test/schemas/recursive/%/schema.metapack +./explorer/test/schemas/recursive/%/schema.metapack.deps ./explorer/test/schemas/string ./explorer/test/schemas/string/% ./explorer/test/schemas/string/%/directory-html.metapack @@ -1936,6 +1966,79 @@ ./schemas/test/schemas/clash/foo/bar/%/schema.metapack.deps ./schemas/test/schemas/clash/foo/bar/%/stats.metapack ./schemas/test/schemas/clash/foo/bar/%/stats.metapack.deps +./schemas/test/schemas/cycle +./schemas/test/schemas/cycle/bar +./schemas/test/schemas/cycle/bar/% +./schemas/test/schemas/cycle/bar/%/blaze-exhaustive.metapack +./schemas/test/schemas/cycle/bar/%/blaze-exhaustive.metapack.deps +./schemas/test/schemas/cycle/bar/%/blaze-fast.metapack +./schemas/test/schemas/cycle/bar/%/blaze-fast.metapack.deps +./schemas/test/schemas/cycle/bar/%/bundle.metapack +./schemas/test/schemas/cycle/bar/%/bundle.metapack.deps +./schemas/test/schemas/cycle/bar/%/dependencies.metapack +./schemas/test/schemas/cycle/bar/%/dependencies.metapack.deps +./schemas/test/schemas/cycle/bar/%/dependents.metapack +./schemas/test/schemas/cycle/bar/%/dependents.metapack.deps +./schemas/test/schemas/cycle/bar/%/editor.metapack +./schemas/test/schemas/cycle/bar/%/editor.metapack.deps +./schemas/test/schemas/cycle/bar/%/health.metapack +./schemas/test/schemas/cycle/bar/%/health.metapack.deps +./schemas/test/schemas/cycle/bar/%/locations.metapack +./schemas/test/schemas/cycle/bar/%/locations.metapack.deps +./schemas/test/schemas/cycle/bar/%/positions.metapack +./schemas/test/schemas/cycle/bar/%/positions.metapack.deps +./schemas/test/schemas/cycle/bar/%/schema.metapack +./schemas/test/schemas/cycle/bar/%/schema.metapack.deps +./schemas/test/schemas/cycle/bar/%/stats.metapack +./schemas/test/schemas/cycle/bar/%/stats.metapack.deps +./schemas/test/schemas/cycle/baz +./schemas/test/schemas/cycle/baz/% +./schemas/test/schemas/cycle/baz/%/blaze-exhaustive.metapack +./schemas/test/schemas/cycle/baz/%/blaze-exhaustive.metapack.deps +./schemas/test/schemas/cycle/baz/%/blaze-fast.metapack +./schemas/test/schemas/cycle/baz/%/blaze-fast.metapack.deps +./schemas/test/schemas/cycle/baz/%/bundle.metapack +./schemas/test/schemas/cycle/baz/%/bundle.metapack.deps +./schemas/test/schemas/cycle/baz/%/dependencies.metapack +./schemas/test/schemas/cycle/baz/%/dependencies.metapack.deps +./schemas/test/schemas/cycle/baz/%/dependents.metapack +./schemas/test/schemas/cycle/baz/%/dependents.metapack.deps +./schemas/test/schemas/cycle/baz/%/editor.metapack +./schemas/test/schemas/cycle/baz/%/editor.metapack.deps +./schemas/test/schemas/cycle/baz/%/health.metapack +./schemas/test/schemas/cycle/baz/%/health.metapack.deps +./schemas/test/schemas/cycle/baz/%/locations.metapack +./schemas/test/schemas/cycle/baz/%/locations.metapack.deps +./schemas/test/schemas/cycle/baz/%/positions.metapack +./schemas/test/schemas/cycle/baz/%/positions.metapack.deps +./schemas/test/schemas/cycle/baz/%/schema.metapack +./schemas/test/schemas/cycle/baz/%/schema.metapack.deps +./schemas/test/schemas/cycle/baz/%/stats.metapack +./schemas/test/schemas/cycle/baz/%/stats.metapack.deps +./schemas/test/schemas/cycle/foo +./schemas/test/schemas/cycle/foo/% +./schemas/test/schemas/cycle/foo/%/blaze-exhaustive.metapack +./schemas/test/schemas/cycle/foo/%/blaze-exhaustive.metapack.deps +./schemas/test/schemas/cycle/foo/%/blaze-fast.metapack +./schemas/test/schemas/cycle/foo/%/blaze-fast.metapack.deps +./schemas/test/schemas/cycle/foo/%/bundle.metapack +./schemas/test/schemas/cycle/foo/%/bundle.metapack.deps +./schemas/test/schemas/cycle/foo/%/dependencies.metapack +./schemas/test/schemas/cycle/foo/%/dependencies.metapack.deps +./schemas/test/schemas/cycle/foo/%/dependents.metapack +./schemas/test/schemas/cycle/foo/%/dependents.metapack.deps +./schemas/test/schemas/cycle/foo/%/editor.metapack +./schemas/test/schemas/cycle/foo/%/editor.metapack.deps +./schemas/test/schemas/cycle/foo/%/health.metapack +./schemas/test/schemas/cycle/foo/%/health.metapack.deps +./schemas/test/schemas/cycle/foo/%/locations.metapack +./schemas/test/schemas/cycle/foo/%/locations.metapack.deps +./schemas/test/schemas/cycle/foo/%/positions.metapack +./schemas/test/schemas/cycle/foo/%/positions.metapack.deps +./schemas/test/schemas/cycle/foo/%/schema.metapack +./schemas/test/schemas/cycle/foo/%/schema.metapack.deps +./schemas/test/schemas/cycle/foo/%/stats.metapack +./schemas/test/schemas/cycle/foo/%/stats.metapack.deps ./schemas/test/schemas/html ./schemas/test/schemas/html/% ./schemas/test/schemas/html/%/blaze-exhaustive.metapack @@ -2128,6 +2231,30 @@ ./schemas/test/schemas/no-schema/%/schema.metapack.deps ./schemas/test/schemas/no-schema/%/stats.metapack ./schemas/test/schemas/no-schema/%/stats.metapack.deps +./schemas/test/schemas/recursive +./schemas/test/schemas/recursive/% +./schemas/test/schemas/recursive/%/blaze-exhaustive.metapack +./schemas/test/schemas/recursive/%/blaze-exhaustive.metapack.deps +./schemas/test/schemas/recursive/%/blaze-fast.metapack +./schemas/test/schemas/recursive/%/blaze-fast.metapack.deps +./schemas/test/schemas/recursive/%/bundle.metapack +./schemas/test/schemas/recursive/%/bundle.metapack.deps +./schemas/test/schemas/recursive/%/dependencies.metapack +./schemas/test/schemas/recursive/%/dependencies.metapack.deps +./schemas/test/schemas/recursive/%/dependents.metapack +./schemas/test/schemas/recursive/%/dependents.metapack.deps +./schemas/test/schemas/recursive/%/editor.metapack +./schemas/test/schemas/recursive/%/editor.metapack.deps +./schemas/test/schemas/recursive/%/health.metapack +./schemas/test/schemas/recursive/%/health.metapack.deps +./schemas/test/schemas/recursive/%/locations.metapack +./schemas/test/schemas/recursive/%/locations.metapack.deps +./schemas/test/schemas/recursive/%/positions.metapack +./schemas/test/schemas/recursive/%/positions.metapack.deps +./schemas/test/schemas/recursive/%/schema.metapack +./schemas/test/schemas/recursive/%/schema.metapack.deps +./schemas/test/schemas/recursive/%/stats.metapack +./schemas/test/schemas/recursive/%/stats.metapack.deps ./schemas/test/schemas/string ./schemas/test/schemas/string/% ./schemas/test/schemas/string/%/blaze-exhaustive.metapack diff --git a/test/sandbox/schemas/test/folder/cycle/bar.json b/test/sandbox/schemas/test/folder/cycle/bar.json new file mode 100644 index 00000000..cb343ef2 --- /dev/null +++ b/test/sandbox/schemas/test/folder/cycle/bar.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": { + "$ref": "./baz.json" + } +} diff --git a/test/sandbox/schemas/test/folder/cycle/baz.json b/test/sandbox/schemas/test/folder/cycle/baz.json new file mode 100644 index 00000000..e0f684c2 --- /dev/null +++ b/test/sandbox/schemas/test/folder/cycle/baz.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": { + "$ref": "./foo.json" + } +} diff --git a/test/sandbox/schemas/test/folder/cycle/foo.json b/test/sandbox/schemas/test/folder/cycle/foo.json new file mode 100644 index 00000000..3a164cf1 --- /dev/null +++ b/test/sandbox/schemas/test/folder/cycle/foo.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": { + "$ref": "./bar.json" + } +} diff --git a/test/sandbox/schemas/test/folder/recursive.json b/test/sandbox/schemas/test/folder/recursive.json new file mode 100644 index 00000000..b74b81a6 --- /dev/null +++ b/test/sandbox/schemas/test/folder/recursive.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": { + "$ref": "#" + } +} diff --git a/test/ui/dependencies.spec.js b/test/ui/dependencies.spec.js new file mode 100644 index 00000000..da39934f --- /dev/null +++ b/test/ui/dependencies.spec.js @@ -0,0 +1,123 @@ +import { test, expect } from '@playwright/test'; + +// Chain: test/bundling/double -> test/bundling/single -> test/v2.0/schema +test.describe('Dependencies and Dependents relationship', () => { + test('single has a direct dependency on v2.0/schema visible in its ' + + 'dependencies tab, and v2.0/schema lists single as a direct dependent', + async ({ page }) => { + // Navigate to test/bundling/single and open the dependencies tab + await page.goto('/test/bundling/single?tab=dependencies'); + const dependenciesPanel = page.locator( + '[data-sourcemeta-ui-tab-id="dependencies"]'); + await expect(dependenciesPanel).not.toHaveClass(/d-none/); + + // It should show 1 direct dependency and 0 indirect + await expect(dependenciesPanel).toContainText('1 direct dependency'); + await expect(dependenciesPanel).toContainText('0 indirect dependencies'); + + // The dependency table should list v2.0/schema + const dependencyRows = dependenciesPanel.locator('tbody tr'); + await expect(dependencyRows).toHaveCount(1); + await expect(dependencyRows.first()).toContainText('/test/v2.0/schema'); + + // Now navigate to that dependency and check its dependents tab + await page.goto('/test/v2.0/schema?tab=dependents'); + const dependentsPanel = page.locator( + '[data-sourcemeta-ui-tab-id="dependents"]'); + await expect(dependentsPanel).not.toHaveClass(/d-none/); + + // single should appear as a direct dependent + await expect(dependentsPanel).toContainText('1 direct dependent'); + const dependentRows = dependentsPanel.locator('tbody tr'); + const singleRow = dependentRows.filter({ + hasText: '/test/bundling/single' + }); + await expect(singleRow).toHaveCount(1); + await expect(singleRow.locator('.badge')).toContainText('Direct'); + }); + + test('double has direct and indirect dependencies, and single lists ' + + 'double as a direct dependent', + async ({ page }) => { + // Navigate to test/bundling/double and open the dependencies tab + await page.goto('/test/bundling/double?tab=dependencies'); + const dependenciesPanel = page.locator( + '[data-sourcemeta-ui-tab-id="dependencies"]'); + await expect(dependenciesPanel).not.toHaveClass(/d-none/); + + // double has 2 direct dependencies (single + draft-07 metaschema) + // and 1 indirect dependency (v2.0/schema via single) + await expect(dependenciesPanel).toContainText('2 direct dependencies'); + await expect(dependenciesPanel).toContainText('1 indirect dependency'); + + // The dependency table should contain single as a direct dependency + const directRows = dependenciesPanel.locator('tbody tr').filter({ + hasText: '/test/bundling/single' + }); + await expect(directRows).toHaveCount(1); + // The row should contain the JSON Pointer, not the "Indirect" badge + await expect(directRows).toContainText('/properties/foo/$ref'); + + // The table should also contain v2.0/schema as indirect + const indirectRows = dependenciesPanel.locator('tbody tr').filter({ + hasText: '/test/v2.0/schema' + }); + await expect(indirectRows).toHaveCount(1); + await expect(indirectRows).toContainText('Indirect'); + + // Now navigate to single and check that double is listed as a dependent + await page.goto('/test/bundling/single?tab=dependents'); + const dependentsPanel = page.locator( + '[data-sourcemeta-ui-tab-id="dependents"]'); + await expect(dependentsPanel).not.toHaveClass(/d-none/); + + // double should appear as a direct dependent of single + const doubleRow = dependentsPanel.locator('tbody tr').filter({ + hasText: '/test/bundling/double' + }); + await expect(doubleRow).toHaveCount(1); + await expect(doubleRow.locator('.badge')).toContainText('Direct'); + }); + + test('v2.0/schema shows transitive dependents with correct types', + async ({ page }) => { + await page.goto('/test/v2.0/schema?tab=dependents'); + const dependentsPanel = page.locator( + '[data-sourcemeta-ui-tab-id="dependents"]'); + await expect(dependentsPanel).not.toHaveClass(/d-none/); + + // single is a direct dependent, while double, with-rebase, and + // with-rebase-same-host are indirect (they depend on single, not + // directly on v2.0/schema) + await expect(dependentsPanel).toContainText('1 direct dependent'); + await expect(dependentsPanel).toContainText('3 indirect dependents'); + + const dependentRows = dependentsPanel.locator('tbody tr'); + await expect(dependentRows).toHaveCount(4); + + // single is the direct dependent + const singleRow = dependentRows.filter({ + hasText: '/test/bundling/single' + }); + await expect(singleRow).toHaveCount(1); + await expect(singleRow.locator('.badge')).toContainText('Direct'); + + // double is an indirect dependent + const doubleRow = dependentRows.filter({ + hasText: '/test/bundling/double' + }); + await expect(doubleRow).toHaveCount(1); + await expect(doubleRow.locator('.badge')).toContainText('Indirect'); + }); + + test('dependents tab badge shows the correct deduplicated count', + async ({ page }) => { + await page.goto('/test/v2.0/schema'); + + const dependentsTabButton = page.locator( + '[data-sourcemeta-ui-tab-target="dependents"]'); + // 1 direct + 3 indirect = 4 unique dependents + const badge = dependentsTabButton.locator('.badge'); + await expect(badge).toContainText('4'); + }); +});