Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions web/src/classHierarchy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, expect, it, vi } from "vitest";

vi.hoisted(() => {
for (const name of ["WebGLRenderingContext", "WebGL2RenderingContext"]) {
Object.defineProperty(globalThis, name, {
configurable: true,
value: class WebGLRenderingContext {},
});
}
});

import {
Children,
isValidElement,
type ReactElement,
type ReactNode,
} from "react";
import { renderToStaticMarkup } from "react-dom/server";
import type { EntityTypeView } from "./api";
import { en } from "./i18n/en";
import { zh } from "./i18n/zh";
import { ClassDefinition } from "./pages/Ontology";

const ROOT_ID = "root";
const DIRECT_CHILD_ID = "direct-child";
const OTHER_ID = "other";
const GRANDCHILD_ID = "grandchild";

type TestElement = ReactElement<{
children?: ReactNode;
onClick?: () => void;
}>;

const elementChildren = (element: TestElement): TestElement[] =>
Children.toArray(element.props.children).filter(isValidElement) as TestElement[];

const entityType = (id: string, parents: string[]): EntityTypeView => ({
id,
key: id,
label: id,
color: "#000000",
shape: "circle",
builtin: false,
parents,
disjoint: [],
primary_parent: parents[0] ?? null,
description: "",
usage: 0,
});

describe("class hierarchy details", () => {
it("renders navigable parent and subclass controls", () => {
const selected = entityType(DIRECT_CHILD_ID, [ROOT_ID]);
const onSelectClass = vi.fn();
const definition = ClassDefinition({
cls: selected,
allTypes: [
entityType(ROOT_ID, []),
selected,
entityType(GRANDCHILD_ID, [DIRECT_CHILD_ID]),
entityType(OTHER_ID, []),
],
onSelectClass,
onNewSub: () => undefined,
}) as TestElement;
const html = renderToStaticMarkup(definition);

expect(html).toContain(`>${ROOT_ID}</button>`);
expect(html).toContain(`>${GRANDCHILD_ID}</button>`);
expect(html).not.toContain(`>${OTHER_ID}</button>`);

const [parentField, subclassField] = elementChildren(definition);
const [parentLink] = elementChildren(elementChildren(parentField)[0]);
const [subclassLink] = elementChildren(elementChildren(subclassField)[0]);
parentLink.props.onClick?.();
subclassLink.props.onClick?.();
expect(onSelectClass.mock.calls).toEqual([[ROOT_ID], [GRANDCHILD_ID]]);

const leafHtml = renderToStaticMarkup(
ClassDefinition({
cls: entityType(OTHER_ID, []),
allTypes: [entityType(OTHER_ID, [])],
onSelectClass,
onNewSub: () => undefined,
}),
);
expect(leafHtml).toContain(">None</div>");
});

it("explains that inheritance is shown on Definition in both locales", () => {
expect(en.ontology.subclasses).toBe("Subclasses");
expect(en.ontology.noSubclasses).toBe("None");
expect(zh.ontology.subclasses).toBe("子类");
expect(zh.ontology.noSubclasses).toBe("无");
expect(en.ontology.schemaNoRelationships).toContain("Definition");
expect(zh.ontology.schemaNoRelationships).toContain("定义");
});
});
5 changes: 4 additions & 1 deletion web/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1278,6 +1278,8 @@ export const en = {
shapeColor: "Shape & color",
parent: "Parent class",
noParent: "(top level)",
subclasses: "Subclasses",
noSubclasses: "None",
disjoint: "Cannot also be",
disjointHint:
"Classes nothing can belong to at the same time. A Person is not an Organisation. The consistency check uses this to find classes that can never have an instance.",
Expand Down Expand Up @@ -1546,7 +1548,8 @@ export const en = {
schemaBundle: (n: number) => `${n} relations`,
schemaOutgoing: "From this class",
schemaIncoming: "To this class",
schemaNoRelationships: "No relationships yet.",
schemaNoRelationships:
"This class takes part in no relationships. Its inheritance is shown under Definition.",
schemaNoInstances: "No instances yet.",
schemaConnectHint: "Connect using an existing relationship",
schemaConnectPlaceholder: "Search relationships…",
Expand Down
4 changes: 3 additions & 1 deletion web/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,8 @@ export const zh: Strings = {
shapeColor: "形状与颜色",
parent: "父类",
noParent: "(顶层)",
subclasses: "子类",
noSubclasses: "无",
disjoint: "不可能同时是",
disjointHint:
"任何东西不可能同时属于的类。人不是组织。一致性检查据此找出永远不可能有实例的类。",
Expand Down Expand Up @@ -1340,7 +1342,7 @@ export const zh: Strings = {
schemaBundle: (n) => `${n} 条关系`,
schemaOutgoing: "从这个类出发",
schemaIncoming: "指向这个类",
schemaNoRelationships: "还没有关系。",
schemaNoRelationships: "这个类没有参与任何关系。继承信息显示在“定义”中。",
schemaNoInstances: "还没有实例。",
schemaConnectHint: "用一个已有的关系连接",
schemaConnectPlaceholder: "搜索关系…",
Expand Down
20 changes: 18 additions & 2 deletions web/src/pages/Ontology.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ export function Ontology() {
<ClassDefinition
cls={selectedClass}
allTypes={entity_types}
onSelectClass={(id) => setSel({ kind: "class", id })}
onNewSub={() =>
setEdit({ kind: "class", existing: null, parentId: selectedClass.id })
}
Expand Down Expand Up @@ -1272,21 +1273,36 @@ function Description({ text }: { text: string | null | undefined }) {
);
}

function ClassDefinition({
export function ClassDefinition({
cls,
allTypes,
onSelectClass,
onNewSub,
}: {
cls: EntityTypeView;
allTypes: EntityTypeView[];
onSelectClass: (id: string) => void;
/** 以当前类为父级新建子类:开弹窗 */
onNewSub: () => void;
}) {
const nameOf = (id: string) => allTypes.find((t) => t.id === id)?.label ?? id;
const classLinks = (ids: string[]) =>
ids.map((id, index) => (
<span key={id}>
{index > 0 && ", "}
<LinkButton onClick={() => onSelectClass(id)}>{nameOf(id)}</LinkButton>
</span>
));
const subclasses = allTypes.filter((type) => type.parents.includes(cls.id));
return (
<div>
<Def label={S.ontology.parent}>
{cls.parents.length > 0 ? cls.parents.map(nameOf).join(", ") : S.ontology.noParent}
{cls.parents.length > 0 ? classLinks(cls.parents) : S.ontology.noParent}
</Def>
<Def label={S.ontology.subclasses}>
{subclasses.length > 0
? classLinks(subclasses.map((type) => type.id))
: S.ontology.noSubclasses}
</Def>
<Def label={S.ontology.disjoint}>
{cls.disjoint.length > 0 ? cls.disjoint.map(nameOf).join(", ") : S.ontology.noDisjoint}
Expand Down
Loading