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
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ jobs:
- name: Install Node Packages
run: npm ci

- name: Run package.json linter
run: npm run lint:pkg

- name: Run linters
run: npm run format

Expand Down
3 changes: 3 additions & 0 deletions .npmpackagejsonlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "npm-package-json-lint-config-default"
}
912 changes: 902 additions & 10 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"dev": "node tests/server.mjs",
"lint:check": "eslint \"**/*.{js,mjs,jsx,ts,tsx}\"",
"lint:fix": "eslint \"**/*.{js,mjs,jsx,ts,tsx}\" --fix",
"lint:pkg": "npmPkgJsonLint \"**/package.json\" --ignore-pattern \"**/node_modules/**\"",
"pretty:check": "prettier --check ./",
"pretty:fix": "prettier --write ./",
"format": "node tests/format.mjs",
Expand Down Expand Up @@ -55,6 +56,9 @@
"lws-log": "^3.0.0",
"lws-static": "^3.1.1",
"mocha": "^10.2.0",
"normalize-package-data": "^8.0.0",
"npm-package-json-lint": "^10.4.1",
"npm-package-json-lint-config-default": "^9.0.1",
"prettier": "^2.8.3",
"prettier-plugin-tailwindcss": "^0.4.1",
"selenium-webdriver": "^4.44.0",
Expand Down
1 change: 1 addition & 0 deletions suites/newssite/news-nuxt/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "nuxt-app",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "node scripts/build.mjs",
Expand Down
1 change: 1 addition & 0 deletions suites/react-stockcharts/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "react-stockcharts-bench",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "react-scripts start",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "todomvc-jquery-complex",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "node scripts/build.mjs",
Expand Down
1 change: 1 addition & 0 deletions suites/todomvc/architecture-examples/jquery/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "todomvc-jquery",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "http-server . -p 7001 -c-1 --cors",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "lit-complex",
"version": "1.0.0",
"private": true,
"engines": {
"node": ">=24.0.0",
Expand Down
1 change: 1 addition & 0 deletions suites/todomvc/architecture-examples/lit/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "todomvc-lit",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
Expand Down
18 changes: 18 additions & 0 deletions tests/setup-node.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
import expect from "expect.js";
import sinon from "sinon";
import { execSync } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export const ROOT_DIR = path.resolve(__dirname, "../");

export function sh(command, options = {}) {
return execSync(command, {
cwd: ROOT_DIR,
encoding: "utf-8",
stdio: ["ignore", "pipe", "ignore"],
...options,
});
}

globalThis.expect = expect;
globalThis.sinon = sinon;
globalThis.sh = sh;
globalThis.ROOT_DIR = ROOT_DIR;

export const mochaHooks = {
afterEach() {
Expand Down
139 changes: 139 additions & 0 deletions tests/unittests/metric.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import expect from "expect.js";
import { Metric } from "../../resources/metric.mjs";

describe("Metric (Data Trees & Serialization)", () => {
describe("constructor & property enumeration", () => {
it("should initialize with required string name and default 'ms' unit", () => {
const metric = new Metric("test-metric");
expect(metric.name).to.be("test-metric");
expect(metric.unit).to.be("ms");
expect(metric.description).to.be("");
expect(metric.values).to.eql([]);
expect(metric.mean).to.be(0);
expect(metric.delta).to.be(0);
expect(metric.percentDelta).to.be(0);
});

it("should accept custom unit strings", () => {
const metric = new Metric("score-metric", "score");
expect(metric.unit).to.be("score");
});

it("should throw an Error if name is not a string", () => {
expect(() => new Metric(123)).to.throwError(/Invalid metric\.name=123, expected string/);
expect(() => new Metric()).to.throwError(/Invalid metric\.name=undefined, expected string/);
});

it("should define geomean, parent, and children as non-enumerable properties to prevent JSON.stringify circular reference crashes", () => {
const root = new Metric("Root");
const child = new Metric("Root-Child");
root.addChild(child);

expect(root.geomean).to.be(0);
expect(root.children).to.contain(child);
expect(child.parent).to.be(root);

const enumeratedKeys = Object.keys(root);
expect(enumeratedKeys).not.to.contain("geomean");
expect(enumeratedKeys).not.to.contain("parent");
expect(enumeratedKeys).not.to.contain("children");

expect(() => JSON.stringify(root)).not.to.throwError();
const serialized = JSON.parse(JSON.stringify(root));
expect(serialized.name).to.be("Root");
expect(serialized.geomean).to.be(undefined);
expect(serialized.parent).to.be(undefined);
expect(serialized.children).to.be(undefined);
});
});

describe("shortName", () => {
it("should return the full name when no parent is set", () => {
const metric = new Metric("Suite-Test");
expect(metric.shortName).to.be("Suite-Test");
});

it("should strip the parent name prefix when parent is set", () => {
const parent = new Metric("Suite");
const child = new Metric("Suite-Test");
parent.addChild(child);
expect(child.shortName).to.be("Test");
});
});

describe("valueString & deltaString", () => {
it("should format mean and unit when delta or percentDelta is zero", () => {
const metric = new Metric("Test");
metric.mean = 10.5;
metric.delta = 0;
metric.percentDelta = 0;
expect(metric.valueString).to.be("10.50 ms");
expect(metric.deltaString).to.be("");
});

it("should format mean, delta, percentDelta, and unit when confidence intervals are computed", () => {
const metric = new Metric("Test");
metric.mean = 100.0;
metric.delta = 2.5;
metric.percentDelta = 2.5;
expect(metric.deltaString).to.be("2.50 (2.5%)");
expect(metric.valueString).to.be("100.00 ± 2.50 (2.5%) ms");
});
});

describe("add() & addChild()", () => {
it("should add valid numeric values and update length", () => {
const metric = new Metric("Test");
metric.add(15);
metric.add(25);
expect(metric.values).to.eql([15, 25]);
expect(metric.length).to.be(2);
});

it("should throw when adding non-numeric values", () => {
const metric = new Metric("Test");
expect(() => metric.add("not a number")).to.throwError(/Adding invalid value=not a number to metric=Test/);
});

it("should throw when re-adding a metric that already has a parent", () => {
const parent1 = new Metric("Parent1");
const parent2 = new Metric("Parent2");
const child = new Metric("Child");
parent1.addChild(child);
expect(() => parent2.addChild(child)).to.throwError(/Cannot re-add sub metric/);
});
});

describe("computeAggregatedMetrics()", () => {
it("should correctly compute sum, min, max, mean, and geomean for numeric samples", () => {
const metric = new Metric("Test");
metric.add(10);
metric.add(20);
metric.add(30);
metric.computeAggregatedMetrics();

expect(metric.sum).to.be(60);
expect(metric.min).to.be(10);
expect(metric.max).to.be(30);
expect(metric.mean).to.be(20);
// Geomean of (10, 20, 30) = (6000)^(1/3) ~ 18.1712
expect(metric.geomean).to.be.within(18.17, 18.18);
expect(metric.delta).to.be.a("number");
expect(metric.percentDelta).to.be.a("number");
});

it("should handle single sample without confidence intervals", () => {
const metric = new Metric("Single");
metric.add(42);
metric.computeAggregatedMetrics();

expect(metric.sum).to.be(42);
expect(metric.min).to.be(42);
expect(metric.max).to.be(42);
expect(metric.mean).to.be(42);
expect(metric.geomean).to.be(42);
expect(metric.delta).to.be(0);
expect(metric.percentDelta).to.be(0);
});
});
});
71 changes: 71 additions & 0 deletions tests/unittests/translations.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import expect from "expect.js";
import { todos, defaultTodoText, defaultLanguage, getTodoText } from "../../resources/shared/translations.mjs";

describe("Translations & Localization (`resources/shared/translations.mjs`)", () => {
const supportedLanguages = ["en", "zh-cn", "ja", "es", "de", "ru", "emoji"];

describe("todos dictionary integrity", () => {
it("should contain all supported language keys", () => {
expect(typeof todos).to.be("object");
expect(todos).not.to.be(null);
for (const lang of supportedLanguages) {
expect(todos).to.have.property(lang);
expect(Array.isArray(todos[lang])).to.be(true, `Expected array for language ${lang}`);
}
});

it("should have identical dictionary lengths across all supported languages (1-to-1 symmetry)", () => {
const baseLength = todos.en.length;
expect(baseLength).to.be.greaterThan(0);

for (const lang of supportedLanguages)
expect(todos[lang].length).to.be(baseLength, `Expected language '${lang}' to have exactly ${baseLength} translations, found ${todos[lang].length}`);
});

it("should contain only valid non-empty strings in all dictionaries", () => {
for (const lang of supportedLanguages) {
for (let i = 0; i < todos[lang].length; i++) {
const text = todos[lang][i];
expect(text).to.be.a("string", `Expected string at index ${i} for language '${lang}'`);
expect(text.trim().length).to.be.greaterThan(0, `Found empty string at index ${i} for language '${lang}'`);
}
}
});
});

describe("defaultTodoText dictionary", () => {
it("should provide default text strings for all supported languages", () => {
expect(typeof defaultTodoText).to.be("object");
for (const lang of supportedLanguages) {
expect(defaultTodoText).to.have.property(lang);
expect(defaultTodoText[lang]).to.be.a("string");
expect(defaultTodoText[lang].trim().length).to.be.greaterThan(0);
}
});
});

describe("defaultLanguage", () => {
it("should export 'en' as defaultLanguage", () => {
expect(defaultLanguage).to.be("en");
});
});

describe("getTodoText() helper", () => {
it("should return exact todo item when index is within bounds", () => {
expect(getTodoText("en", 0)).to.be(todos.en[0]);
expect(getTodoText("ja", 5)).to.be(todos.ja[5]);
expect(getTodoText("de", 10)).to.be(todos.de[10]);
});

it("should wrap index modulo dictionary length when index exceeds length", () => {
const len = todos.en.length;
expect(getTodoText("en", len)).to.be(todos.en[0]);
expect(getTodoText("en", len + 3)).to.be(todos.en[3]);
expect(getTodoText("es", len * 2 + 7)).to.be(todos.es[7]);
});

it("should fall back to 'en' when lang parameter is not passed or undefined", () => {
expect(getTodoText(undefined, 2)).to.be(todos.en[2]);
});
});
});
Loading