ifcurl/forgejo/tests/viewer-url.test.js
Bruno Postle d60bd044dd forgejo: add JS unit tests, extract pure functions into viewer-util.js
Extract parseCommitHref, isSimpleTypeSelector, subtractIdMap, and
isCommitHash into a new viewer-util.js ES module so they can be tested
without a browser. Convert ifcurl.js from an IIFE to an ES module
importing from viewer-util.js; update footer.tmpl to type="module".

Add node:test suites for viewer-url.js (32 tests) and viewer-util.js
(28 tests). Add npm test script and document it in the build section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 20:24:58 +01:00

203 lines
8 KiB
JavaScript

// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { parseIfcUrl, buildIfcUrl, toRawUrl } from "../custom/public/assets/viewer-url.js";
// ---------------------------------------------------------------------------
// parseIfcUrl
// ---------------------------------------------------------------------------
describe("parseIfcUrl", () => {
test("returns null for non-ifc URLs", () => {
assert.equal(parseIfcUrl("https://example.com"), null);
assert.equal(parseIfcUrl(""), null);
assert.equal(parseIfcUrl(null), null);
assert.equal(parseIfcUrl(undefined), null);
});
test("parses host and repo path", () => {
const r = parseIfcUrl("ifc://example.com/org/repo@heads/main?path=model.ifc");
assert.equal(r.host, "example.com");
assert.equal(r.repoSuffix, "org/repo");
assert.equal(r.userAt, "");
});
test("parses user@ authority", () => {
const r = parseIfcUrl("ifc://git@example.com/org/repo@heads/main?path=model.ifc");
assert.equal(r.userAt, "git@");
assert.equal(r.host, "example.com");
assert.equal(r.repo, "git@example.com/org/repo");
});
test("parses heads/ ref", () => {
const r = parseIfcUrl("ifc://example.com/org/repo@heads/main?path=a.ifc");
assert.equal(r.ref, "heads/main");
});
test("parses tags/ ref", () => {
const r = parseIfcUrl("ifc://example.com/org/repo@tags/v1.0?path=a.ifc");
assert.equal(r.ref, "tags/v1.0");
});
test("parses bare commit hash ref", () => {
const r = parseIfcUrl("ifc://example.com/org/repo@abc123def456?path=a.ifc");
assert.equal(r.ref, "abc123def456");
});
test("parses path parameter", () => {
const r = parseIfcUrl("ifc://example.com/org/repo@heads/main?path=subdir/model.ifc");
assert.equal(r.path, "subdir/model.ifc");
});
test("parses selector parameter", () => {
const r = parseIfcUrl("ifc://example.com/org/repo@heads/main?selector=IfcWall");
assert.equal(r.selector, "IfcWall");
});
test("parses camera parameter", () => {
const r = parseIfcUrl("ifc://example.com/org/repo@heads/main?camera=1,2,3,4,5,6,7,8,9");
assert.equal(r.camera, "1,2,3,4,5,6,7,8,9");
});
test("parses fov parameter", () => {
const r = parseIfcUrl("ifc://example.com/org/repo@heads/main?fov=60");
assert.equal(r.fov, "60");
});
test("parses scale parameter", () => {
const r = parseIfcUrl("ifc://example.com/org/repo@heads/main?scale=0.01");
assert.equal(r.scale, "0.01");
});
test("defaults missing parameters to empty string", () => {
const r = parseIfcUrl("ifc://example.com/org/repo@heads/main");
assert.equal(r.path, "");
assert.equal(r.selector, "");
assert.equal(r.camera, "");
assert.equal(r.fov, "");
assert.equal(r.scale, "");
});
test("builds repo field as userAt + host + / + repoSuffix", () => {
const r = parseIfcUrl("ifc://example.com/org/repo@heads/main");
assert.equal(r.repo, "example.com/org/repo");
});
});
// ---------------------------------------------------------------------------
// buildIfcUrl
// ---------------------------------------------------------------------------
describe("buildIfcUrl", () => {
test("returns null when repo is missing", () => {
assert.equal(buildIfcUrl("", "heads/main", "model.ifc", ""), null);
assert.equal(buildIfcUrl(null, "heads/main", "", ""), null);
});
test("returns null when ref is missing", () => {
assert.equal(buildIfcUrl("example.com/org/repo", "", "model.ifc", ""), null);
assert.equal(buildIfcUrl("example.com/org/repo", null, "", ""), null);
});
test("builds minimal URL with no path or selector", () => {
const url = buildIfcUrl("example.com/org/repo", "heads/main", "", "");
assert.equal(url, "ifc://example.com/org/repo@heads/main");
});
test("includes path in query string", () => {
const url = buildIfcUrl("example.com/org/repo", "heads/main", "model.ifc", "");
assert.equal(url, "ifc://example.com/org/repo@heads/main?path=model.ifc");
});
test("includes selector in query string", () => {
const url = buildIfcUrl("example.com/org/repo", "heads/main", "", "IfcWall");
assert.equal(url, "ifc://example.com/org/repo@heads/main?selector=IfcWall");
});
test("includes both path and selector", () => {
const url = buildIfcUrl("example.com/org/repo", "heads/main", "model.ifc", "IfcWall");
assert.equal(url, "ifc://example.com/org/repo@heads/main?path=model.ifc&selector=IfcWall");
});
test("does not percent-encode commas in selector", () => {
const url = buildIfcUrl("example.com/org/repo", "heads/main", "", "IfcWall,Name=\"Core\"");
assert.ok(url.includes(","), "comma should not be encoded");
assert.ok(!url.includes("%2C"), "%2C should not appear");
});
test("does not percent-encode + in selector", () => {
const url = buildIfcUrl("example.com/org/repo", "heads/main", "", "IfcWall+IfcSlab");
assert.ok(!url.includes("%2B"), "%2B should not appear");
});
test("does not percent-encode $ in selector (GlobalId syntax)", () => {
const url = buildIfcUrl("example.com/org/repo", "heads/main", "", "325Q7Fhnf67OZC$$r43uzK");
assert.ok(!url.includes("%24"), "%24 should not appear");
});
});
// ---------------------------------------------------------------------------
// toRawUrl
// ---------------------------------------------------------------------------
describe("toRawUrl", () => {
// GitHub
test("generates GitHub raw URL for heads/ ref", () => {
const url = toRawUrl("ifc://github.com/org/repo@heads/main?path=model.ifc");
assert.equal(url, "https://raw.githubusercontent.com/org/repo/main/model.ifc");
});
test("generates GitHub raw URL for tags/ ref", () => {
const url = toRawUrl("ifc://github.com/org/repo@tags/v1.0?path=model.ifc");
assert.equal(url, "https://raw.githubusercontent.com/org/repo/v1.0/model.ifc");
});
test("generates GitHub raw URL for bare commit hash", () => {
const url = toRawUrl("ifc://github.com/org/repo@abc123?path=model.ifc");
assert.equal(url, "https://raw.githubusercontent.com/org/repo/abc123/model.ifc");
});
// GitLab
test("generates GitLab raw URL for heads/ ref", () => {
const url = toRawUrl("ifc://gitlab.com/org/repo@heads/main?path=model.ifc");
assert.equal(url, "https://gitlab.com/org/repo/-/raw/main/model.ifc");
});
test("generates GitLab raw URL for tags/ ref", () => {
const url = toRawUrl("ifc://gitlab.com/org/repo@tags/v1.0?path=model.ifc");
assert.equal(url, "https://gitlab.com/org/repo/-/raw/v1.0/model.ifc");
});
// Forgejo / Gitea (codeberg.org, self-hosted, etc.)
test("generates Forgejo raw URL for heads/ ref", () => {
const url = toRawUrl("ifc://codeberg.org/org/repo@heads/main?path=model.ifc");
assert.equal(url, "https://codeberg.org/org/repo/raw/branch/main/model.ifc");
});
test("generates Forgejo raw URL for tags/ ref", () => {
const url = toRawUrl("ifc://codeberg.org/org/repo@tags/v1.0?path=model.ifc");
assert.equal(url, "https://codeberg.org/org/repo/raw/tag/v1.0/model.ifc");
});
test("generates Forgejo raw URL for bare commit hash", () => {
const url = toRawUrl("ifc://codeberg.org/org/repo@abc123?path=model.ifc");
assert.equal(url, "https://codeberg.org/org/repo/raw/commit/abc123/model.ifc");
});
test("generates Forgejo raw URL for self-hosted instance", () => {
const url = toRawUrl("ifc://git.example.com/org/repo@heads/main?path=model.ifc");
assert.equal(url, "https://git.example.com/org/repo/raw/branch/main/model.ifc");
});
test("uses http for localhost Forgejo (local dev instance)", () => {
const url = toRawUrl("ifc://localhost:3000/org/repo@heads/main?path=model.ifc");
assert.equal(url, "http://localhost:3000/org/repo/raw/branch/main/model.ifc");
});
test("includes subdirectory file path", () => {
const url = toRawUrl("ifc://codeberg.org/org/repo@heads/main?path=subdir/model.ifc");
assert.ok(url.endsWith("/subdir/model.ifc"), `expected path suffix, got: ${url}`);
});
});