Merge pull request #821 from Cooldude2606/feature/version-selector

Add drop down version selector based on Wube version archive
This commit is contained in:
Daniel authored and GitHub committed 2026-01-25 20:07:38 +01:00
commit c9fc4f1c8f
24 files changed
+2245 -9

No files matched your search

File diff suppressed because it is too large. Load diff
+127
View File
@@ -0,0 +1,127 @@
const assert = require("assert").strict;
const { ValueCache, wait } = require("@clusterio/lib");
describe("ValueCache", function () {
describe("constructor", function () {
it("constructs with a fetch function", function() {
const fetcher = async () => 42;
const cache = new ValueCache(fetcher);
assert.ok(cache);
});
});
describe("get", function() {
it("throws TypeError for negative maxAgeMs", async function() {
const fetcher = async () => 42;
const cache = new ValueCache(fetcher);
assert.throws(() => cache.get(-1), err => {
assert.equal(err instanceof TypeError, true);
assert.match(String(err.message), /maxAgeMs/);
return true;
});
});
it("uses a default of 0 for maxAgeMs", async function() {
let callCount = 0;
const fetcher = async () => {
callCount += 1;
return callCount;
};
const cache = new ValueCache(fetcher);
const v1 = await cache.get();
const v2 = await cache.get();
assert.equal(v1, 1);
assert.equal(v2, 2);
assert.equal(callCount, 2);
});
it("returns cached value when within maxAgeMs", async function() {
let callCount = 0;
const fetcher = async () => {
callCount += 1;
return callCount;
};
const cache = new ValueCache(fetcher);
const v1 = await cache.get(1000);
const v2 = await cache.get(1000);
assert.equal(v1, 1);
assert.equal(v2, 1);
assert.equal(callCount, 1);
});
it("refreshes the cache when older than maxAgeMs", async function() {
let callCount = 0;
const fetcher = async () => {
callCount += 1;
return callCount;
};
const cache = new ValueCache(fetcher);
const v1 = await cache.get(50);
const v2 = await cache.get(1000); // Use cached value
await wait(100);
const v3 = await cache.get(50); // Stale after 100ms wait
const v4 = await cache.get(1000); // Use cached value
assert.equal(v1, 1);
assert.equal(v2, 1);
assert.equal(v3, 2);
assert.equal(v4, 2);
assert.equal(callCount, 2);
});
it("shares in-flight fetch between concurrent callers", async function() {
let callCount = 0;
let resolveFetcher;
const fetcher = () => (
new Promise(resolve => {
callCount += 1;
resolveFetcher = resolve;
})
);
const cache = new ValueCache(fetcher);
const p1 = cache.get(1000);
const p2 = cache.get(1000);
assert.equal(callCount, 1);
resolveFetcher(42);
const [v1, v2] = await Promise.all([p1, p2]);
assert.equal(v1, 42);
assert.equal(v2, 42);
assert.equal(callCount, 1);
});
it("propagates errors from the fetcher and clears ongoing fetch", async function() {
let callCount = 0;
const error = new Error("fetch failed");
const fetcher = async () => {
callCount += 1;
throw error;
};
const cache = new ValueCache(fetcher);
await assert.rejects(cache.get(), err => {
assert.equal(err, error);
return true;
});
assert.equal(callCount, 1);
// After a failure, a new attempt to fetch should be made
await assert.rejects(cache.get(), err => {
assert.equal(err, error);
return true;
});
assert.equal(callCount, 2);
});
});
});
+134
View File
@@ -0,0 +1,134 @@
const assert = require("assert").strict;
const { readFile } = require("fs-extra");
const { fetchFactorioVersions } = require("@clusterio/lib");
const { slowTest } = require("../../integration");
describe("FactorioVersions", function() {
describe("fetchFactorioVersions", function() {
const originalFetch = global.fetch;
beforeEach(() => {
global.fetch = undefined;
});
afterEach(() => {
global.fetch = originalFetch;
});
function mockFetch(html) {
const mockResponse = {
ok: true,
status: 200,
statusText: "OK",
text: async () => html,
};
global.fetch = async () => mockResponse;
}
it("fetches and parsed versions with stable / experimental flags", async function() {
mockFetch(`
<html>
<body>
<a class="
slot-button-inline
version-button-experimental
" href="/download/archive/2.0.73">
2.0.73
</a>
</body>
<body>
<a class="
slot-button-inline
version-button-stable
" href="/download/archive/2.0.72">
2.0.72
</a>
</body>
</html>
`);
const versions = await fetchFactorioVersions();
assert.deepEqual(versions, [
{
stable: false,
version: "2.0.73",
headlessUrl: "www.factorio.com/get-download/2.0.73/headless/linux64",
},
{
stable: true,
version: "2.0.72",
headlessUrl: "www.factorio.com/get-download/2.0.72/headless/linux64",
},
]);
});
it("deduplicates versions by last occurrence", async function() {
mockFetch(`
<html>
<body>
<a class="
slot-button-inline
version-button-experimental
" href="/download/archive/2.0.73">
2.0.73
</a>
</body>
<body>
<a class="
slot-button-inline
version-button-stable
" href="/download/archive/2.0.73">
2.0.73
</a>
</body>
</html>
`);
const versions = await fetchFactorioVersions();
assert.deepEqual(versions, [
{
stable: true,
version: "2.0.73",
headlessUrl: "www.factorio.com/get-download/2.0.73/headless/linux64",
},
]);
});
it("thorws on HTTP error status", async function() {
const mockResponse = {
ok: false,
status: 500,
statusText: "Server Error",
text: async () => "",
};
global.fetch = async () => mockResponse;
await assert.rejects(fetchFactorioVersions(), /HTTP 500 Server Error/);
});
it("thorws on network error status", async function() {
global.fetch = async () => {
throw new Error("Network Error");
};
await assert.rejects(fetchFactorioVersions(), /Network Error/);
});
it("parses versions from saved html file", async function() {
mockFetch(await readFile("test/file/html/factorio-download-archive.html"));
const versions = await fetchFactorioVersions();
assert.equal(versions.length, 105);
assert.equal(versions.filter(v => v.stable).length, 48);
assert.equal(versions.filter(v => !v.stable).length, 57);
});
it("parses versions from live api", async function() {
slowTest(this);
global.fetch = originalFetch;
const versions = await fetchFactorioVersions();
assert.ok(versions.length >= 105);
assert.ok(versions.filter(v => v.stable).length >= 48);
assert.ok(versions.filter(v => !v.stable).length >= 57);
});
});
});
+40
View File
@@ -34,4 +34,44 @@ describe("messages/controller", function() {
);
});
});
describe("External", function() {
it("runs", async function() {
let callCount = 0;
const versions = [
{ version: "1.2.3", stable: true, headlessUrl: "path/to/download" },
{ version: "1.2.4", stable: false, headlessUrl: "path/to/download" },
];
// Mock the cache to avoid needless fetches
controller.factorioVersions = new lib.ValueCache(async () => {
callCount += 1;
return versions;
});
// First call should result in a fetch
const versions1 = await controlConnection.handleFactorioVersionsRequest(
new lib.FactorioVersionsRequest()
);
assert.deepEqual(versions1, versions);
assert.equal(callCount, 1);
// Second call should not fetch due to default maxAgeMs being greater than 0
const versions2 = await controlConnection.handleFactorioVersionsRequest(
new lib.FactorioVersionsRequest()
);
assert.deepEqual(versions2, versions);
assert.equal(callCount, 1);
// Setting maxAgeMs to 0 should result in a new fetch
const versions3 = await controlConnection.handleFactorioVersionsRequest(
new lib.FactorioVersionsRequest(0)
);
assert.deepEqual(versions3, versions);
assert.equal(callCount, 2);
});
});
});