Skip to content

Commit 5d2ea41

Browse files
authored
Add widget.js smoke test (#202)
* Add widget smoke test with vitest browser mode Smoke-tests the HiGlass anywidget render pipeline in headless Chromium via vitest browser mode and Playwright. SwiftShader args are required since PixiJS needs a WebGL context unavailable in headless by default. * Just install chromium in CI * Use `onTestFinished` instead of try/catch
1 parent 2bc1429 commit 5d2ea41

7 files changed

Lines changed: 121 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ jobs:
5050
deno fmt --check
5151
deno lint
5252
53+
TestJavaScript:
54+
name: JavaScript / Test
55+
runs-on: ubuntu-latest
56+
steps:
57+
- uses: actions/checkout@v5
58+
- uses: denoland/setup-deno@v2
59+
with:
60+
deno-version: v2.x
61+
- run: deno install
62+
- run: npx playwright install chromium
63+
- run: deno task vitest --run
64+
5365
TypecheckJavaScript:
5466
name: JavaScript / Typecheck
5567
runs-on: ubuntu-latest

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ dist/
1111
.venv
1212
examples/test.mcool
1313
*.bigWig
14+
node_modules
15+
__screenshots__

deno.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"lock": false,
3+
"nodeModulesDir": "auto",
34
"compilerOptions": {
45
"checkJs": true,
56
"lib": ["dom", "dom.iterable", "esnext"]
@@ -11,5 +12,15 @@
1112
},
1213
"fmt": {
1314
"exclude": ["examples"]
15+
},
16+
"tasks": {
17+
"vitest": "deno run -A vitest"
18+
},
19+
"imports": {
20+
"@anywidget/types": "npm:@anywidget/types@^0.2.0",
21+
"@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.6",
22+
"@vitest/browser-playwright": "npm:@vitest/browser-playwright@^4.0.18",
23+
"vite": "npm:vite@^7.3.1",
24+
"vitest": "npm:vitest@^4.0.18"
1425
}
1526
}

src/higlass/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,7 @@ export type GenomicLocation = {
8282

8383
/** Partial types for the viewconf */
8484
export type Viewconf = {
85-
views: Array<{ uid: string }>;
85+
views: Array<
86+
{ uid: string; layout?: unknown; tracks?: unknown }
87+
>;
8688
};

src/higlass/widget.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as hglib from "https://esm.sh/higlass@1.13?deps=react@17,react-dom@17,pixi.js@6";
22
import { v4 } from "https://esm.sh/@lukeed/uuid@2.0.1";
33

4+
/** @import { AnyModel } from "@anywidget/types" */
45
/** @import { PluginDataFetcherConstructor, GenomicLocation, Viewconf, DataFetcher} from "./types.ts" */
56

67
const NAME = "jupyter";
@@ -111,7 +112,7 @@ function assert(expression, msg = "") {
111112
* ```
112113
*
113114
* @template T
114-
* @param {import("npm:@anywidget/types").AnyModel} model
115+
* @param {AnyModel} model
115116
* @param {{ payload: unknown, signal?: AbortSignal }} options
116117
* @return {Promise<{ payload: T, buffers: Array<DataView> }>}
117118
*/
@@ -192,7 +193,7 @@ function resolveJupyterServers(viewConfig) {
192193
}
193194

194195
/**
195-
* @param {import("npm:@anywidget/types@0.2.0").AnyModel<State>} model */
196+
* @param {AnyModel<State>} model */
196197
async function registerJupyterHiGlassDataFetcher(model) {
197198
if (window?.higlassDataFetchersByType?.[NAME]) {
198199
return;
@@ -292,7 +293,7 @@ function addEventListenersTo(el) {
292293
*/
293294

294295
export default {
295-
/** @type {import("npm:@anywidget/types").Render<State>} */
296+
/** @type {import("@anywidget/types").Render<State>} */
296297
async render({ model, el }) {
297298
await Promise.all([
298299
requireScripts(model.get("_plugin_urls")),

src/higlass/widget.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { expect, onTestFinished, test, vi } from "vitest";
2+
import type { AnyModel, Experimental } from "@anywidget/types";
3+
import type { State } from "./widget.js";
4+
5+
const experimental: Experimental = {
6+
invoke() {
7+
throw new Error("experimental.invoke is not implemented.");
8+
},
9+
};
10+
11+
test("render creates a HiGlass viewer with a simple viewconf", async () => {
12+
const { default: widget } = await import("./widget.js");
13+
14+
const el = document.createElement("div");
15+
el.style.width = "800px";
16+
el.style.height = "400px";
17+
document.body.appendChild(el);
18+
onTestFinished(() => el.remove());
19+
20+
const tilesetModel = {
21+
on: vi.fn(),
22+
off: vi.fn(),
23+
send: vi.fn(),
24+
};
25+
26+
const model: AnyModel<State> = {
27+
get(key) {
28+
const state: State = {
29+
_plugin_urls: [],
30+
_viewconf: {
31+
views: [{
32+
uid: "v",
33+
layout: { x: 0, y: 0, w: 12, h: 6 },
34+
tracks: {
35+
top: [{ type: "top-axis", uid: "t" }],
36+
center: [],
37+
left: [],
38+
right: [],
39+
bottom: [],
40+
},
41+
}],
42+
},
43+
_options: {},
44+
_tileset_client: "IPY_MODEL_fake",
45+
location: [0, 0, 0, 0],
46+
};
47+
return state[key];
48+
},
49+
set: vi.fn(),
50+
save_changes: vi.fn(),
51+
on: vi.fn(),
52+
off: vi.fn(),
53+
send: vi.fn(),
54+
widget_manager: {
55+
get_model: vi.fn().mockResolvedValue(tilesetModel),
56+
},
57+
};
58+
59+
const cleanup = await widget.render({ model, el, experimental });
60+
61+
// resolved without throwing; container has content
62+
expect(el.children.length).toBeGreaterThan(0);
63+
64+
expect(typeof cleanup).toBe("function");
65+
cleanup?.();
66+
});

vite.config.mjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { defineConfig } from "vitest/config";
2+
3+
import { playwright } from "@vitest/browser-playwright";
4+
import deno from "@deno/vite-plugin";
5+
6+
export default defineConfig({
7+
plugins: [deno()],
8+
test: {
9+
globals: true,
10+
browser: {
11+
provider: playwright({
12+
launchOptions: {
13+
// needed to access WebGL in headless
14+
args: ["--use-gl=angle", "--use-angle=swiftshader"],
15+
},
16+
}),
17+
enabled: true,
18+
instances: [
19+
{ browser: "chromium", headless: true },
20+
],
21+
},
22+
},
23+
});

0 commit comments

Comments
 (0)