From e6f0a2da61c4050b5866cbdc157f67b9ce67e4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20H=C3=B6ffner?= Date: Thu, 19 Oct 2023 10:35:26 +0200 Subject: [PATCH] More refactoring and linting improvement. Fix Cytoscape test. --- .eslintrc.json | 5 ++++- js/browser/chaptersearch.ts | 2 +- js/browser/contextmenu.ts | 2 +- js/browser/contextmenuEdges.ts | 4 ++-- js/browser/contextmenuNodes.ts | 2 +- js/browser/graph.ts | 15 ++++++++------- js/browser/load.ts | 2 +- js/browser/save.ts | 6 +++--- js/browser/state.ts | 1 + js/browser/view.ts | 9 +++++---- js/config.dist.ts | 2 +- js/layout.ts | 21 ++++++++------------- js/loadGraphFromSparql.ts | 12 ++++++------ package.json | 3 ++- test/cytoscape.test.ts | 12 ++++-------- test/fuse.test.ts | 9 ++------- tsconfig.json | 2 +- 17 files changed, 51 insertions(+), 58 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 19af4d39..b3498a3a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -37,6 +37,9 @@ "@typescript-eslint/no-explicit-any": ["off"], "@typescript-eslint/no-this-alias": ["warn", { "allowedNames": ["thisView"] }], "@typescript-eslint/no-shadow": ["warn"], + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/consistent-type-imports": ["error"], + "@typescript-eslint/consistent-type-exports": ["error"], "no-shadow": ["off"], "no-console": ["off"], "no-duplicate-imports": ["error"], @@ -45,6 +48,7 @@ "eol-last": ["error"], "import/no-default-export": ["warn"], "import/no-cycle": ["warn"], + "import/no-unresolved": ["error"], "max-len": ["warn", 245], "padded-blocks": ["error", "never"], "space-in-parens": ["error"], @@ -55,7 +59,6 @@ "no-control-regex": ["off"], "curly": ["warn"], "camelcase": ["warn"], - "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], "prefer-const": ["warn"], "comma-style": ["error", "last"], "class-methods-use-this": ["off"], diff --git a/js/browser/chaptersearch.ts b/js/browser/chaptersearch.ts index 7b483084..2461f300 100644 --- a/js/browser/chaptersearch.ts +++ b/js/browser/chaptersearch.ts @@ -2,7 +2,7 @@ import * as sparql from "../sparql"; import * as util from "./util"; import * as language from "../lang/language"; -import { Graph } from "./graph"; +import type { Graph } from "./graph"; import MicroModal from "micromodal"; const chapters: Map> = new Map(); diff --git a/js/browser/contextmenu.ts b/js/browser/contextmenu.ts index 1f649828..14c955d9 100644 --- a/js/browser/contextmenu.ts +++ b/js/browser/contextmenu.ts @@ -1,7 +1,7 @@ /** Creates the circular context menu that can be opened on top of a node/edge. Needs to be initialized before it can be used via the default export function.*/ import { flatHelp } from "../help"; -import { MenuItem } from "./menuItem"; +import type { MenuItem } from "./menuItem"; import cytoscape from "cytoscape"; //{ Collection, EdgeSingular, NodeSingular, SingularElementReturnValue } from "cytoscape"; import contextMenus from "cytoscape-context-menus"; diff --git a/js/browser/contextmenuEdges.ts b/js/browser/contextmenuEdges.ts index 987df798..50b87d66 100644 --- a/js/browser/contextmenuEdges.ts +++ b/js/browser/contextmenuEdges.ts @@ -4,9 +4,9 @@ import * as util from "./util"; import { EDGE } from "../edge"; import * as language from "../lang/language"; import { Graph } from "./graph"; -import { ContextMenu, MenuItem } from "./contextmenu"; +import { type MenuItem, ContextMenu } from "./contextmenu"; import log from "loglevel"; -import { EdgeSingular } from "cytoscape"; +import type { EdgeSingular } from "cytoscape"; /** Creates a human readable string of the triple that an edge represents. * @param edge - the edge, whose label is determined diff --git a/js/browser/contextmenuNodes.ts b/js/browser/contextmenuNodes.ts index 2e4130f6..4973e0f5 100644 --- a/js/browser/contextmenuNodes.ts +++ b/js/browser/contextmenuNodes.ts @@ -4,7 +4,7 @@ import * as rdf from "../rdf"; import { NODE } from "../node"; import * as util from "./util"; import { Graph, Direction } from "./graph"; -import { ContextMenu, MenuItem } from "./contextmenu"; +import { type MenuItem, ContextMenu } from "./contextmenu"; import * as sparql from "../sparql"; import * as language from "../lang/language"; diff --git a/js/browser/graph.ts b/js/browser/graph.ts index e2dd0b80..702a28d7 100644 --- a/js/browser/graph.ts +++ b/js/browser/graph.ts @@ -11,7 +11,8 @@ import * as language from "../lang/language"; import { progress } from "./progress"; import { View } from "./view"; import MicroModal from "micromodal"; -import cytoscape, { Collection, NodeCollection, EdgeCollection, NodeSingular } from "cytoscape"; +import type { Core, Collection, NodeCollection, EdgeCollection, NodeSingular } from "cytoscape"; +import cytoscape from "cytoscape"; //eslint-disable-line no-duplicate-imports import type { Menu } from "./menu"; import log from "loglevel"; @@ -27,11 +28,11 @@ export enum Direction { /** Cytoscape.js Graph Class with path operations and styling. */ export class Graph { - cy: cytoscape.Core; - selectedNode: cytoscape.NodeSingular | null = null; + cy: Core; + selectedNode: NodeSingular | null = null; starMode: boolean = false; - matchComponents: Array = []; - pathSource: cytoscape.NodeSingular | null = null; + matchComponents: Array = []; + pathSource: NodeSingular | null = null; container: HTMLElement; instancesLoaded: boolean = false; menu: Menu = null; @@ -84,7 +85,7 @@ export class Graph { } /** @param eles - the elements to assign the star mode css class to */ - static starStyle(eles: cytoscape.Collection): void { + static starStyle(eles: Collection): void { eles.removeClass("hidden"); //eles.addClass('starmode'); eles.select(); @@ -587,7 +588,7 @@ export class Graph { } /**Show close matches of the given nodes. * @param nodes - the nodes whose close matches are shown */ - showCloseMatch(nodes: cytoscape.NodeCollection): void { + showCloseMatch(nodes: NodeCollection): void { const edges = nodes.connectedEdges(".unfiltered").filter('[pl="closeMatch"]'); // ,[pl="narrowMatch"],[pl="narrowMatch"] const matches = edges.connectedNodes(".unfiltered"); log.debug( diff --git a/js/browser/load.ts b/js/browser/load.ts index 8c60371b..1596f510 100644 --- a/js/browser/load.ts +++ b/js/browser/load.ts @@ -1,6 +1,6 @@ /** Module for loading files both locally from the server and via upload from the client.*/ import { View } from "./view"; -import { ViewJson, Session } from "./interface"; +import type { ViewJson, Session } from "./interface"; import { config } from "../config"; import { fromJSON } from "./state"; import { VERSION } from "./util"; diff --git a/js/browser/save.ts b/js/browser/save.ts index 59636d08..e1444257 100644 --- a/js/browser/save.ts +++ b/js/browser/save.ts @@ -1,10 +1,10 @@ /** Lets the user save files generated from the loaded graph. */ -import { ViewJson, Session } from "./interface"; +import type { ViewJson, Session } from "./interface"; import { config } from "../config"; import { toJSON } from "./state"; -import { View, State } from "./view"; +import { View, type State } from "./view"; import { VERSION } from "./util"; -import { Graph } from "./graph"; +import type { Graph } from "./graph"; import log from "loglevel"; import c from "cytoscape"; import svg from "cytoscape-svg"; diff --git a/js/browser/state.ts b/js/browser/state.ts index af3bd3d3..be0f3aa4 100644 --- a/js/browser/state.ts +++ b/js/browser/state.ts @@ -1,3 +1,4 @@ +/** Application State, e.g. which filters and options are activated, which is imported from and exported to JSON. */ import { Filter } from "./filter"; import { menu } from "./menu"; import { VERSION } from "./util"; diff --git a/js/browser/view.ts b/js/browser/view.ts index 2152a62c..a71ef86f 100644 --- a/js/browser/view.ts +++ b/js/browser/view.ts @@ -6,7 +6,7 @@ import { edgeCommands } from "./contextmenuEdges"; import { goldenLayout } from "./viewLayout"; import { toJSON } from "./state"; import log from "loglevel"; -import { ComponentConfig, ContentItem } from "golden-layout"; +import type { ComponentConfig, ContentItem } from "golden-layout"; let viewCount: number = 0; // only used for the name, don't decrement on destroy to prevent name conflicts @@ -31,16 +31,17 @@ function traverse(x: ContentItem, depth: number): Array { return removeTabsArray; } -export interface State { +interface ViewState { title: string; graph: Graph; name: string; cy: cytoscape.Core; } +/** Tab that shows a graph. */ export class View { initialized: Promise; - state: State; + state: ViewState; readonly cyContainer: HTMLDivElement = document.createElement("div"); element: HTMLElement; cxtMenu: ContextMenu; @@ -53,7 +54,7 @@ export class View { /** Returns the state of the active (focussed) view. @returns The state of the active (focussed) view. */ - static activeState(): State { + static activeState(): ViewState { return (viewLayout as any).selectedItem?.getActiveContentItem()?.config?.componentState; } diff --git a/js/config.dist.ts b/js/config.dist.ts index 653643e1..ff215b31 100644 --- a/js/config.dist.ts +++ b/js/config.dist.ts @@ -2,7 +2,7 @@ Copy to js/config.ts after checkout and adapt to your preferences. */ -import { LogLevelDesc } from "loglevel"; +import type { LogLevelDesc } from "loglevel"; export const config = { defaultSubOntologies: ["meta", "bb", "ob", "ciox", "he", "it4it"], diff --git a/js/layout.ts b/js/layout.ts index e5a46353..0e58fc5c 100644 --- a/js/layout.ts +++ b/js/layout.ts @@ -7,13 +7,14 @@ import { timer } from "./timer"; import { NODE } from "./node"; import { config } from "./config"; import log from "loglevel"; -import cytoscape, { ElementDefinition, LayoutOptions, NodeCollection } from "cytoscape"; +import type { ElementDefinition, LayoutOptions, NodeCollection, Layouts, Position } from "cytoscape"; +import cytoscape from "cytoscape"; //eslint-disable-line no-duplicate-imports import cytoscapeeuler from "cytoscape-euler"; cytoscape.use(cytoscapeeuler); const ANIMATE_THRESHOLD = 500; -let activeLayout: cytoscape.Layouts; +let activeLayout: Layouts; /** @param layoutName - Cytoscape.js layout name @@ -52,7 +53,7 @@ export function positions(nodes: NodeCollection): Array> { /** @param nodes - the nodes whose center is returned @returns the center point of the nodes */ -function center(nodes: cytoscape.NodeCollection): cytoscape.Position { +function center(nodes: NodeCollection): Position { const c = { x: 0.0, y: 0.0 }; for (let i = 0; i < nodes.length; i++) { const pos = nodes[i].position(); @@ -76,20 +77,14 @@ function center(nodes: cytoscape.NodeCollection): cytoscape.Position { run(cy,{"name":"grid"},new Set(["meta","ciox"])) ``` */ -export async function run( - cy: cytoscape.Core, - layoutConfig: LayoutOptions, - subs?: Array, - separateSubs: boolean = false, - save: boolean = false -): Promise { +export async function run(cy: Core, layoutConfig: LayoutOptions, subs?: Array, separateSubs: boolean = false, save: boolean = false): Promise { if (cy.nodes().size() === 0) { log.warn("layout.js#run: Graph empty. Nothing to layout."); return false; } const layoutTimer = timer("layout"); if (separateSubs) { - const sources: Set = new Set(); + const sources: Set = new Set(); const virtualEdges: Array = []; const nodes = cy.nodes(); @@ -168,7 +163,7 @@ in pos are set to position `{x:0,y:0}`, positions without matching node id are i @returns whether the layout could be successfully applied @example `presetLayout(cy,[["http://www.snik.eu...",{"x":0,"y":0}],...]);` */ -export async function presetLayout(cy: cytoscape.Core, pos: Array): Promise { +export async function presetLayout(cy: Core, pos: Array): Promise { const map = new Map(pos as any); let hits = 0; let misses = 0; @@ -318,7 +313,7 @@ export function eulerVariable(len) */ /** Layout for compound graphs */ -export const cose: cytoscape.LayoutOptions = { +export const cose: LayoutOptions = { name: "cose", animate: true, refresh: 50, diff --git a/js/loadGraphFromSparql.ts b/js/loadGraphFromSparql.ts index 5c0e6c60..a86cbebf 100644 --- a/js/loadGraphFromSparql.ts +++ b/js/loadGraphFromSparql.ts @@ -3,7 +3,7 @@ import * as sparql from "./sparql"; import { timer } from "./timer"; import { config } from "./config"; import log from "loglevel"; -import cytoscape, { ElementDefinition } from "cytoscape"; +import type { ElementDefinition, Core } from "cytoscape"; interface ClassBinding { src: { value: string }; @@ -83,7 +83,7 @@ function parseLabels(s: string): object { * @param from - a SPARQL FROM clause defining where to load the classes from * @returns nodes - representing the classes */ -async function createClassNodes(from: string): Promise> { +async function createClassNodes(from: string): Promise> { const bindings = await selectClasses(from); const nodes: Array = []; @@ -146,7 +146,7 @@ async function selectInstances(from: string): Promise> { @returns cytoscape nodes for the instances */ async function createInstanceNodes(from: string): Promise> { const json = await selectInstances(from); - const nodes: Array = []; + const nodes: Array = []; for (let i = 0; i < json.length; i++) { nodes.push({ group: "nodes", @@ -210,7 +210,7 @@ async function selectTriples(from: string, fromNamed: string, instances: boolean * @param virtual - whether to select virtual triples from domain and range statements * @returns SPARQL query result object */ -async function createEdges(from: string, fromNamed: string, instances: boolean, virtual: boolean): Promise> { +async function createEdges(from: string, fromNamed: string, instances: boolean, virtual: boolean): Promise> { const json = await selectTriples(from, fromNamed, instances, virtual); const edges: Array = []; for (let i = 0; i < json.length; i++) { @@ -238,7 +238,7 @@ async function createEdges(from: string, fromNamed: string, instances: boolean, * @param instances - whether to load instances in addition to the classes * @returns an array of nodes */ -async function createNodes(from: string, instances: boolean): Promise> { +async function createNodes(from: string, instances: boolean): Promise> { if (!instances) { return createClassNodes(from); } @@ -256,7 +256,7 @@ async function createNodes(from: string, instances: boolean): Promise, instances: boolean = false, virtual: boolean = false): Promise { +export async function loadGraphFromSparql(cy: Core, graphs: Array, instances: boolean = false, virtual: boolean = false): Promise { log.debug(`Loading graph from endpoint ${config.sparql.endpoint} with graphs ${graphs}.`); const from = graphs.map((g) => `FROM <${g}>`).reduce((a, b) => a + "\n" + b, ""); const fromNamed = from.replace(/FROM/g, "FROM NAMED"); diff --git a/package.json b/package.json index 407c285a..3d25ba7f 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "c8": "^8.0.1", "eslint": "^8.51.0", "eslint-plugin-import": "^2.28.1", + "eslint-plugin-mocha": "^10.2.0", "eslint-plugin-tsdoc": "^0.2.17", "husky": "^8.0.3", "lint-staged": "^15.0.1", @@ -64,7 +65,7 @@ "preview": "vite preview", "test": "vitest run", "vitest": "vitest", - "lint": "eslint js --ext .ts", + "lint": "eslint js test --ext .ts", "typecheck": "tsc --noEmit", "doc": "typedoc --plugin typedoc-plugin-merge-modules", "prepare": "husky install" diff --git a/test/cytoscape.test.ts b/test/cytoscape.test.ts index 317f650a..9e625510 100644 --- a/test/cytoscape.test.ts +++ b/test/cytoscape.test.ts @@ -2,9 +2,7 @@ import * as layout from "../js/layout"; import { loadGraphFromSparql } from "../js/loadGraphFromSparql"; import { SNIK } from "../js/sparql"; import cytoscape from "cytoscape"; -import euler from "cytoscape-euler"; import "isomorphic-fetch"; -cytoscape.use(euler); import chai from "chai"; const assert = chai.assert; @@ -22,20 +20,18 @@ describe("cytoscape", () => { assert.closeTo(cy.nodes().size(), 1134, 100); }); test("calculate layout", async () => { - // Causes "TypeError: Cannot read property 'pos' of undefined" - // see https://github.com/cytoscape/cytoscape.js-euler/issues/14 - //layout.run(cy,layout.euler,subs); + assert(await layout.run(cy, layout.euler, subs)); // cose is more realistic for SNIK Graph but takes over a minute - //assert(layout.run(cy,layout.cose,subs)); + //assert(await layout.run(cy,layout.cose,subs)); // use the faster grid layout - await layout.run(cy, layout.grid, subs); + //await layout.run(cy, layout.grid); const nodes = cy.nodes(); for (let i = 0; i < nodes.size(); i++) { for (let j = i + 1; j < nodes.size(); j += 10) { assert( JSON.stringify(nodes[i].position()) !== JSON.stringify(nodes[j].position()), - "2 nodes at the same position " + JSON.stringify(nodes[i].position()) + `2 nodes ${i} and ${j} at the same position ` + JSON.stringify(nodes[i].position()) ); } } diff --git a/test/fuse.test.ts b/test/fuse.test.ts index 34299940..864891a3 100644 --- a/test/fuse.test.ts +++ b/test/fuse.test.ts @@ -1,10 +1,5 @@ -import { executionAsyncId } from "async_hooks"; -import { should } from "chai"; import "isomorphic-fetch"; -import { search, createIndex } from "../js/fuse"; -//import chai from "chai"; -//chai.should(); -//const assert = chai.assert; +import { search, createIndex } from "../js/fuseSearch"; console.groupCollapsed; const SEARCH = { @@ -22,7 +17,7 @@ describe("fuse#search", () => { for (const key in SEARCH) { const uris = (await search(key)).map((x) => x.item.uri); const expectedUris = SEARCH[key]; - if (expectedUris.length == 1) { + if (expectedUris.length === 1) { expect(uris).toEqual(expectedUris); } else { expect(uris).toEqual(expect.arrayContaining(expectedUris)); diff --git a/tsconfig.json b/tsconfig.json index 3984d503..72aac4f0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,6 @@ "out": "doc", "readme": "none" }, - "include": ["js/**/*", "globals.d.ts"], + "include": ["js/**/*", "test/**/*", "globals.d.ts"], "exclude": ["node_modules"] }