test: support typescript on qunit tests

Rename qunit-tests.js to .ts and stop hanging our functions on the main
QUnit object (which isn't typesafe).  We can just export them from our
module.  Adjust the users accordingly.

Add explicit file extensions to the list of tests in files.js (removing
the hardcoded '.js' from build.js) and add our first .ts case.

This new case tests that `cockpit.assert()` works properly in the
positive and negative cases and also verifies that it can be used for
type narrowing (otherwise tsc would complain).
This commit is contained in:
Allison Karlitskaya 2024-04-16 14:15:52 +02:00
parent bfc4186b2a
commit 18ec3fa7f0
13 changed files with 129 additions and 103 deletions

View File

@ -115,7 +115,7 @@ async function build() {
const { entryPoints, assetFiles, redhat_fonts } = getFiles(args.onlydir);
const tests = getTestFiles();
const testEntryPoints = tests.map(test => "pkg/" + test + ".js");
const testEntryPoints = tests.map(test => "pkg/" + test);
const pkgFirstPlugins = [
cleanPlugin({ subdir: args.onlydir }),

View File

@ -42,44 +42,45 @@ const info = {
],
tests: [
"base1/test-base64",
"base1/test-browser-storage",
"base1/test-cache",
"base1/test-chan",
"base1/test-dbus-address",
"base1/test-dbus-framed",
"base1/test-dbus",
"base1/test-echo",
"base1/test-events",
"base1/test-external",
"base1/test-file",
"base1/test-format",
"base1/test-framed-cache",
"base1/test-framed",
"base1/test-http",
"base1/test-journal-renderer",
"base1/test-locale",
"base1/test-location",
"base1/test-metrics",
"base1/test-no-jquery",
"base1/test-permissions",
"base1/test-promise",
"base1/test-protocol",
"base1/test-series",
"base1/test-spawn-proc",
"base1/test-spawn",
"base1/test-stream",
"base1/test-user",
"base1/test-utf8",
"base1/test-websocket",
"base1/test-base64.js",
"base1/test-browser-storage.js",
"base1/test-cache.js",
"base1/test-chan.js",
"base1/test-dbus-address.js",
"base1/test-dbus-framed.js",
"base1/test-dbus.js",
"base1/test-echo.js",
"base1/test-events.js",
"base1/test-external.js",
"base1/test-file.js",
"base1/test-format.js",
"base1/test-framed-cache.js",
"base1/test-framed.js",
"base1/test-http.js",
"base1/test-journal-renderer.js",
"base1/test-locale.js",
"base1/test-location.js",
"base1/test-metrics.js",
"base1/test-no-jquery.js",
"base1/test-permissions.js",
"base1/test-promise.js",
"base1/test-protocol.js",
"base1/test-series.js",
"base1/test-spawn-proc.js",
"base1/test-spawn.js",
"base1/test-stream.js",
"base1/test-types.ts",
"base1/test-user.js",
"base1/test-utf8.js",
"base1/test-websocket.js",
"kdump/test-config-client",
"kdump/test-config-client.js",
"networkmanager/test-utils",
"networkmanager/test-utils.js",
"shell/machines/test-machines",
"shell/machines/test-machines.js",
"storaged/test-util",
"storaged/test-util.js",
],
files: [

@ -1 +1 @@
Subproject commit 64c1a675c17c19e1379f436db0aa2b3d1a5f36bb
Subproject commit e5738c52f9b4dcb835dcb9afd3b370ae08c03c78

View File

@ -26,6 +26,7 @@
},
"devDependencies": {
"@types/deep-equal": "1.0.4",
"@types/qunit": "^2.19.10",
"@types/react": "18.2.78",
"@types/react-dom": "18.2.25",
"@typescript-eslint/eslint-plugin": "7.7.0",

View File

@ -18,7 +18,7 @@
*/
import cockpit from "cockpit";
import QUnit from "qunit-tests";
import QUnit, { skipWithPybridge } from "qunit-tests";
function deep_update(target, data) {
for (const prop in data) {
@ -104,7 +104,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
], "round trip");
});
QUnit.test.skipWithPybridge("integer bounds", async assert => {
skipWithPybridge("integer bounds", async assert => {
const dbus = cockpit.dbus(bus_name, channel_options);
async function testNumber(type, value, valid) {
@ -163,7 +163,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
], "round trip");
});
QUnit.test.skipWithPybridge("variants", async assert => {
skipWithPybridge("variants", async assert => {
const dbus = cockpit.dbus(bus_name, channel_options);
const reply = await dbus.call(
"/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
@ -179,7 +179,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
], "round trip");
});
QUnit.test.skipWithPybridge("bad variants", async assert => {
skipWithPybridge("bad variants", async assert => {
const dbus = cockpit.dbus(bus_name, channel_options);
try {
await dbus.call(
@ -236,7 +236,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
}
});
QUnit.test.skipWithPybridge("call bad base64", async assert => {
skipWithPybridge("call bad base64", async assert => {
const dbus = cockpit.dbus(bus_name, channel_options);
try {
await dbus.call(
@ -322,7 +322,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
});
});
QUnit.test.skipWithPybridge("empty base64", assert => {
skipWithPybridge("empty base64", assert => {
const done = assert.async();
assert.expect(3);
@ -340,7 +340,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
});
});
QUnit.test.skipWithPybridge("bad object path", async assert => {
skipWithPybridge("bad object path", async assert => {
const dbus = cockpit.dbus(bus_name, channel_options);
try {
await dbus.call("invalid/path", "borkety.Bork", "Echo", [1]);
@ -351,7 +351,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
}
});
QUnit.test.skipWithPybridge("bad interface name", async assert => {
skipWithPybridge("bad interface name", async assert => {
const dbus = cockpit.dbus(bus_name, channel_options);
try {
await dbus.call("/path", "!invalid!interface!", "Echo", [1]);
@ -362,7 +362,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
}
});
QUnit.test.skipWithPybridge("bad method name", async assert => {
skipWithPybridge("bad method name", async assert => {
const dbus = cockpit.dbus(bus_name, channel_options);
try {
await dbus.call("/path", "borkety.Bork", "!Invalid!Method!", [1]);
@ -373,7 +373,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
}
});
QUnit.test.skipWithPybridge("bad flags", async assert => {
skipWithPybridge("bad flags", async assert => {
const dbus = cockpit.dbus(bus_name, channel_options);
try {
await dbus.call("/path", "borkety.Bork", "Method", [1], { flags: 5 });
@ -384,7 +384,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
}
});
QUnit.test.skipWithPybridge("bad types", async assert => {
skipWithPybridge("bad types", async assert => {
const dbus = cockpit.dbus(bus_name, channel_options);
try {
await dbus.call("/bork", "borkety.Bork", "Echo", [1], { type: "!!%%" });
@ -395,7 +395,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
}
});
QUnit.test.skipWithPybridge("bad type invalid", async assert => {
skipWithPybridge("bad type invalid", async assert => {
const dbus = cockpit.dbus(bus_name, channel_options);
try {
await dbus.call("/bork", "borkety.Bork", "Echo", [1], { type: 5 }); // invalid
@ -406,7 +406,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
}
});
QUnit.test.skipWithPybridge("bad dict type", async assert => {
skipWithPybridge("bad dict type", async assert => {
const dbus = cockpit.dbus(bus_name, channel_options);
try {
await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "Nobody",
@ -418,7 +418,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
}
});
QUnit.test.skipWithPybridge("bad object path", async assert => {
skipWithPybridge("bad object path", async assert => {
const dbus = cockpit.dbus(bus_name, channel_options);
try {
await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "Nobody",
@ -430,7 +430,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
}
});
QUnit.test.skipWithPybridge("bad signature", async assert => {
skipWithPybridge("bad signature", async assert => {
const dbus = cockpit.dbus(bus_name, channel_options);
try {
await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "Nobody", ["bad signature"], { type: "g" });
@ -710,7 +710,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
dbus.removeEventListener("notify", onnotify);
});
QUnit.test.skipWithPybridge("path loop", async assert => {
skipWithPybridge("path loop", async assert => {
const name = "yo" + new Date().getTime();
const cache = { };
@ -733,7 +733,7 @@ export function common_dbus_tests(channel_options, bus_name) { // eslint-disable
dbus.removeEventListener("notify", onnotify);
});
QUnit.test.skipWithPybridge("path signal", async assert => {
skipWithPybridge("path signal", async assert => {
const name = "yo" + new Date().getTime();
const cache = { };
@ -940,7 +940,7 @@ export function dbus_track_tests(channel_options, bus_name) {
assert.equal(gone, false, "is not gone");
});
QUnit.test.skipWithPybridge("receive readable fd", async assert => {
skipWithPybridge("receive readable fd", async assert => {
const done = assert.async();
assert.expect(3);
@ -959,7 +959,7 @@ export function dbus_track_tests(channel_options, bus_name) {
};
});
QUnit.test.skipWithPybridge("receive readable fd and ensure opening more than once fails", async assert => {
skipWithPybridge("receive readable fd and ensure opening more than once fails", async assert => {
const done = assert.async();
assert.expect(6);
@ -981,7 +981,7 @@ export function dbus_track_tests(channel_options, bus_name) {
};
});
QUnit.test.skipWithPybridge("receive readable fd and ensure writing fails", async assert => {
skipWithPybridge("receive readable fd and ensure writing fails", async assert => {
const done = assert.async();
assert.expect(5);
@ -1002,7 +1002,7 @@ export function dbus_track_tests(channel_options, bus_name) {
};
});
QUnit.test.skipWithPybridge("receive writable fd", async assert => {
skipWithPybridge("receive writable fd", async assert => {
const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", channel_options);
const [fd] = await dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber",
"MakeTestFd", ["writable"]);

View File

@ -1,5 +1,5 @@
import cockpit from "cockpit";
import QUnit from "qunit-tests";
import QUnit, { mock_info, skipWithPybridge } from "qunit-tests";
import { common_dbus_tests, dbus_track_tests } from "./test-dbus-common.js";
@ -142,7 +142,7 @@ QUnit.test("owned message for absent service", assert => {
});
});
QUnit.test.skipWithPybridge("bad dbus address", function (assert) {
skipWithPybridge("bad dbus address", function (assert) {
const done = assert.async();
assert.expect(1);
@ -153,7 +153,7 @@ QUnit.test.skipWithPybridge("bad dbus address", function (assert) {
});
});
QUnit.test.skipWithPybridge("bad dbus bus", function (assert) {
skipWithPybridge("bad dbus bus", function (assert) {
const done = assert.async();
assert.expect(1);
@ -194,7 +194,7 @@ QUnit.test("wait fail", function (assert) {
});
});
QUnit.test.skipWithPybridge("no default name", function (assert) {
skipWithPybridge("no default name", function (assert) {
const done = assert.async();
assert.expect(1);
@ -211,7 +211,7 @@ QUnit.test.skipWithPybridge("no default name", function (assert) {
});
});
QUnit.test.skipWithPybridge("no default name bad", function (assert) {
skipWithPybridge("no default name bad", function (assert) {
const done = assert.async();
assert.expect(2);
@ -229,7 +229,7 @@ QUnit.test.skipWithPybridge("no default name bad", function (assert) {
});
});
QUnit.test.skipWithPybridge("no default name invalid", function (assert) {
skipWithPybridge("no default name invalid", function (assert) {
const done = assert.async();
assert.expect(2);
@ -247,7 +247,7 @@ QUnit.test.skipWithPybridge("no default name invalid", function (assert) {
});
});
QUnit.test.skipWithPybridge("no default name missing", function (assert) {
skipWithPybridge("no default name missing", function (assert) {
const done = assert.async();
assert.expect(2);
@ -265,7 +265,7 @@ QUnit.test.skipWithPybridge("no default name missing", function (assert) {
});
});
QUnit.test.skipWithPybridge("no default name second", function (assert) {
skipWithPybridge("no default name second", function (assert) {
const done = assert.async();
assert.expect(2);
@ -290,7 +290,7 @@ QUnit.test.skipWithPybridge("no default name second", function (assert) {
});
});
QUnit.test.skipWithPybridge("override default name", function (assert) {
skipWithPybridge("override default name", function (assert) {
const done = assert.async();
assert.expect(2);
@ -314,7 +314,7 @@ QUnit.test.skipWithPybridge("override default name", function (assert) {
});
});
QUnit.test.skipWithPybridge("watch no default name", function (assert) {
skipWithPybridge("watch no default name", function (assert) {
const done = assert.async();
assert.expect(1);
@ -337,7 +337,7 @@ QUnit.test.skipWithPybridge("watch no default name", function (assert) {
});
});
QUnit.test.skipWithPybridge("watch missing name", function (assert) {
skipWithPybridge("watch missing name", function (assert) {
const done = assert.async();
assert.expect(2);
@ -355,7 +355,7 @@ QUnit.test.skipWithPybridge("watch missing name", function (assert) {
});
});
QUnit.test.skipWithPybridge("shared client", function (assert) {
skipWithPybridge("shared client", function (assert) {
const done = assert.async();
assert.expect(2);
@ -395,7 +395,7 @@ QUnit.test("not shared option", function (assert) {
dbus2.close();
});
QUnit.test.skipWithPybridge("emit signal type", function (assert) {
skipWithPybridge("emit signal type", function (assert) {
const done = assert.async();
assert.expect(4);
@ -425,7 +425,7 @@ QUnit.test.skipWithPybridge("emit signal type", function (assert) {
});
});
QUnit.test.skipWithPybridge("emit signal no meta", function (assert) {
skipWithPybridge("emit signal no meta", function (assert) {
const done = assert.async();
assert.expect(2);
@ -451,13 +451,13 @@ async function internal_test(assert, options) {
QUnit.test("internal dbus", async assert => internal_test(assert, { bus: "internal" }));
QUnit.test.skipWithPybridge("internal dbus bus none",
async assert => internal_test(assert, { bus: "none" }));
skipWithPybridge("internal dbus bus none",
async assert => internal_test(assert, { bus: "none" }));
QUnit.test.skipWithPybridge("internal dbus bus none with address",
async assert => internal_test(assert, { bus: "none", address: "internal" }));
skipWithPybridge("internal dbus bus none with address",
async assert => internal_test(assert, { bus: "none", address: "internal" }));
QUnit.test.skipWithPybridge("separate dbus connections for channel groups", function (assert) {
skipWithPybridge("separate dbus connections for channel groups", function (assert) {
const done = assert.async();
assert.expect(4);
@ -533,7 +533,7 @@ QUnit.test("nonexisting address", async assert => {
await dbus.call("/org/freedesktop/DBus", "org.freedesktop.DBus", "Hello", []);
assert.ok(false, "should not be reached");
} catch (ex) {
if (await QUnit.mock_info("pybridge")) {
if (await mock_info("pybridge")) {
assert.equal(ex.problem, "protocol-error", "got right close code");
assert.equal(ex.message, "failed to connect to none bus: [Errno 2] sd_bus_start: No such file or directory",
"error message");

View File

@ -1,5 +1,5 @@
import cockpit from "cockpit";
import QUnit from "qunit-tests";
import QUnit, { mock_info } from "qunit-tests";
QUnit.test("basic", function (assert) {
const done = assert.async();
@ -83,7 +83,7 @@ QUnit.test("fence", async assert => {
const done = assert.async();
// This is implemented in the C bridge, but not in Python.
if (await QUnit.mock_info("pybridge")) {
if (await mock_info("pybridge")) {
assert.ok(true, "skipping on python bridge, not implemented");
done();
return;

View File

@ -1,5 +1,5 @@
import cockpit from "cockpit";
import QUnit from "qunit-tests";
import QUnit, { mock_info } from "qunit-tests";
const EXPECT_MOCK_STREAM = "0 1 2 3 4 5 6 7 8 9 ";
@ -257,7 +257,7 @@ QUnit.test("http keep alive", async assert => {
assert.expect(1);
// connection sharing is not implemented in the pybridge
if (await QUnit.mock_info("pybridge")) {
if (await mock_info("pybridge")) {
assert.rejects(
cockpit.http({ port: test_server.port, connection: "one" }).get("/mock/connection"),
ex => ex.problem == "protocol-error" && ex.status == undefined,
@ -278,7 +278,7 @@ QUnit.test("http connection different", async assert => {
assert.expect(1);
// connection sharing is not implemented in the pybridge
if (await QUnit.mock_info("pybridge")) {
if (await mock_info("pybridge")) {
assert.ok(true);
return;
}
@ -296,7 +296,7 @@ QUnit.test("http connection without address", async assert => {
assert.expect(1);
// connection sharing is not implemented in the pybridge
if (await QUnit.mock_info("pybridge")) {
if (await mock_info("pybridge")) {
assert.ok(true);
return;
}
@ -363,7 +363,7 @@ QUnit.test("wrong options", async assert => {
"rejects request with both port and unix option");
// This is disallowed in the pybridge, but allowed in the C bridge
if (await QUnit.mock_info("pybridge")) {
if (await mock_info("pybridge")) {
assert.rejects(
cockpit.http({ unix: "/nonexisting/socket", tls: {} }).get("/"),
ex => ex.problem == "protocol-error" && ex.status == undefined,
@ -375,7 +375,7 @@ QUnit.test("wrong options", async assert => {
QUnit.test("parallel stress test", async assert => {
// This is way too slow under valgrind
if (await QUnit.mock_info("skip_slow_tests")) {
if (await mock_info("skip_slow_tests")) {
assert.ok(true, "skipping on python bridge, not implemented");
return;
}

View File

@ -1,5 +1,5 @@
import cockpit from "cockpit";
import QUnit from "qunit-tests";
import QUnit, { mock_info } from "qunit-tests";
const QS_REQUEST = "HEAD /mock/qs HTTP/1.0\nHOST: localhost\n\n";
@ -12,7 +12,7 @@ QUnit.test("TCP stream port without a service", async assert => {
const done = assert.async();
assert.expect(2);
const is_pybridge = await QUnit.mock_info("pybridge");
const is_pybridge = await mock_info("pybridge");
const channel = cockpit.channel({ payload: "stream", address: "127.0.0.99", port: 2222 });

21
pkg/base1/test-types.ts Normal file
View File

@ -0,0 +1,21 @@
import cockpit from 'cockpit';
import QUnit from 'qunit-tests';
function as_str(value: string | number): string {
cockpit.assert(typeof value === "string");
return value; // only (statically) possible because of the assert
}
QUnit.test("cockpit.assert success", function(assert) {
as_str("abc");
assert.ok(true);
});
QUnit.test("cockpit.assert fail", function(assert) {
assert.throws(function() {
as_str(123);
});
assert.ok(true);
});
QUnit.start();

3
pkg/lib/qunit-tap.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
declare module 'qunit-tap' {
export default function qunitTap(qunitObject: QUnit, printLikeFunction: (message: string, ...args: unknown[]) => void, options?: unknown): void;
}

View File

@ -19,11 +19,11 @@
"use strict";
import QUnit from "qunit/qunit/qunit.js";
import qunitTap from "qunit-tap/lib/qunit-tap.js";
import QUnit from "qunit";
import qunitTap from "qunit-tap";
import "qunit/qunit/qunit.css";
QUnit.mock_info = async key => {
export const mock_info = async (key: string) => {
const response = await fetch(`http://${window.location.hostname}:${window.location.port}/mock/info`);
return (await response.json())[key];
};
@ -31,14 +31,14 @@ QUnit.mock_info = async key => {
// Convenience for skipping tests that the python bridge can't yet
// handle.
let is_pybridge = null;
let is_pybridge: boolean | null = null;
QUnit.test.skipWithPybridge = async (name, callback) => {
export const skipWithPybridge = async (name: string, callback: (assert: unknown) => void | Promise<void>) => {
if (is_pybridge === null)
is_pybridge = await QUnit.mock_info("pybridge");
is_pybridge = await mock_info("pybridge");
if (is_pybridge)
QUnit.test.skip(name, callback);
QUnit.skip(name, callback);
else
QUnit.test(name, callback);
};
@ -62,7 +62,7 @@ window.setTimeout(() => {
/* QUnit-Tap writes the summary line right after this function returns.
* Delay printing the end marker until after that summary is out.
*/
QUnit.done(() => window.setTimeout(() => console.log("cockpittest-tap-done"), 0));
QUnit.done(() => { window.setTimeout(() => console.log("cockpittest-tap-done"), 0) });
/* Now initialize qunit-tap
*
@ -76,15 +76,15 @@ QUnit.done(() => window.setTimeout(() => console.log("cockpittest-tap-done"), 0)
* We also want to insert the current test name into all tap lines.
*/
const tap_regex = /^((not )?ok [0-9]+ (- )?)(.*)$/;
qunitTap(QUnit, function() {
if (arguments.length == 1 && QUnit.config.current) {
const match = tap_regex.exec(arguments[0]);
qunitTap(QUnit, function(message: string, ...args: unknown[]) {
if (args.length == 0 && QUnit.config.current) {
const match = tap_regex.exec(message);
if (match) {
console.log(match[1] + QUnit.config.current.testName + ": " + match[4]);
return;
}
}
console.log.apply(console, arguments);
console.log(message, args);
});
export default QUnit;

View File

@ -1,5 +1,5 @@
import cockpit from "cockpit";
import QUnit from "qunit-tests";
import QUnit, { skipWithPybridge } from "qunit-tests";
import { machines } from "./machines.js";
const dbus = cockpit.dbus(null, { bus: "internal" });
@ -54,9 +54,9 @@ QUnit.test("two definitions", async assert => machinesParseTest(
QUnit.test("invalid json", async assert => machinesParseTest(assert, { "01.json": '{"green":' }, {}));
QUnit.test.skipWithPybridge("invalid data types", async assert => machinesParseTest(assert, { "01.json": '{"green": []}' }, {}));
skipWithPybridge("invalid data types", async assert => machinesParseTest(assert, { "01.json": '{"green": []}' }, {}));
QUnit.test.skipWithPybridge("merge several JSON files", async assert => machinesParseTest(
skipWithPybridge("merge several JSON files", async assert => machinesParseTest(
assert,
/* 99-webui.json changes a property in green, adds a
* property to blue, and adds an entire new host yellow */
@ -83,7 +83,7 @@ QUnit.test.skipWithPybridge("merge several JSON files", async assert => machines
}
));
QUnit.test.skipWithPybridge("merge JSON files with errors", async assert => machinesParseTest(
skipWithPybridge("merge JSON files with errors", async assert => machinesParseTest(
assert,
{
"01-valid.json": '{"green": {"visible": true, "address": "1.2.3.4"}}',