fix tests and change invitation mechanics
Sonarqube Scanner / Prepare environment (push) Successful in 8s
Sonarqube Scanner / Build and test (push) Successful in 24s
Sonarqube Scanner / SonarQube analysis (push) Failing after 1m2s
Sonarqube Scanner / Deploy to Server (push) Successful in 6s

This commit is contained in:
Florian Zumpe
2026-10-19 17:21:50 +02:00
parent a66a795407
commit cacc63be66
13 changed files with 349 additions and 146 deletions
+72 -17
View File
@@ -27,17 +27,18 @@ function key(node, value) {
}
async function harness(loading = false) {
const ids = ['group-join-open', 'editor-group-join', 'editor-group-list', 'group-dialog', 'group-close',
'group-entry-dialog', 'group-entry-close', 'group-entry-status', 'group-route',
'group-entry-title', 'group-entry-dialog', 'group-entry-close', 'group-entry-status', 'group-route',
'group-status', 'group-wheels', 'group-entry', 'group-invitation', 'group-code', 'group-join', 'group-edit', 'group-expiry'];
const nodes = new Map(ids.map(id => [id, new Element()]));
const document = new EventTarget();
document.readyState = loading ? 'loading' : 'complete';
document.getElementById = id => nodes.get(id);
document.createElement = () => new Element();
const calls = [], edits = [];
const calls = [], edits = [], timers = [];
const controls = { error: 0, offline: false, confirm: true, active: true, role: 'owner' };
const group = () => ({ id: 'group-id', route: { id: 22, name: 'Waldweg' }, revision: 0, role: controls.role, active: controls.active });
const context = { Event, document, Date, Map, Set, Headers, console, globalThis: null,
const context = { Event, document, Date, Map, Set, Headers, console, crypto, globalThis: null,
setInterval: (callback, delay) => { timers.push({ callback, delay }); },
confirm: () => controls.confirm, matchMedia: () => ({ matches: false }),
Wegwichtel: { Config: { apiBase: '/api/' }, LocalRoutes: { creatorId: () => 'a'.repeat(64) },
EditorUi: { async openGroup(route) { edits.push(route.id); } } },
@@ -48,7 +49,7 @@ async function harness(loading = false) {
let value;
if (options.method === 'GET') value = { groups: [group()] };
else if (options.method === 'DELETE') { controls.active = false; value = { closed: true }; }
else if (url.endsWith('/join')) value = { group: group() };
else if (url.endsWith('/join') || url.endsWith('/edit')) value = { group: group() };
else value = { group: group(), code: Array(5).fill('🐦'), expiresAt: 1800000000 };
return { ok: true, json: async () => value };
}
@@ -59,7 +60,7 @@ async function harness(loading = false) {
const filename = path.resolve('public/js', module + '.js');
vm.runInNewContext(await fs.readFile(filename, 'utf8'), context, { filename });
}
return { nodes, context, calls, controls, edits, groups: context.Wegwichtel.Groups };
return { nodes, context, calls, controls, edits, timers, groups: context.Wegwichtel.Groups };
}
test('group code wheels support arrows, keyboard wrapping, scrolling and joining with ordered Unicode', async () => {
@@ -99,7 +100,7 @@ test('group code wheels support arrows, keyboard wrapping, scrolling and joining
test('sharing displays five motifs and the owner can renew or close access with confirmation', async () => {
const h = await harness();
await h.groups.share(7);
await h.groups.share(7, Array(5).fill('🐦'));
assert.equal(JSON.parse(h.calls[0].body).routeId, 7);
assert.equal(h.nodes.get('group-code').children.length, 5);
assert.equal(h.nodes.get('group-entry-dialog').open, false);
@@ -114,11 +115,10 @@ test('sharing displays five motifs and the owner can renew or close access with
pointer(h.nodes.get('group-edit')); await tick();
assert.deepEqual(h.edits, [22]);
let actions = h.nodes.get('editor-group-list').children[0].children[1].children;
h.controls.confirm = false;
pointer(actions[2]); await tick();
assert.equal(h.calls.some(call => call.url.endsWith('/code')), false);
h.controls.confirm = true;
pointer(actions[2]); await tick();
assert.equal(h.nodes.get('group-entry-dialog').open, true);
pointer(h.nodes.get('group-join')); await tick();
assert.ok(h.calls.some(call => call.url.endsWith('/code')));
actions = h.nodes.get('editor-group-list').children[0].children[1].children;
pointer(actions[1]); await tick();
@@ -192,9 +192,9 @@ test('unsent station changes are listed before network access and discarded only
assert.equal(list.children.length, 0);
});
test('sharing opens the actual five-symbol invitation directly without a two-symbol preview', async () => {
test('sharing a confirmed code shows the five-symbol invitation', async () => {
const h = await harness();
await h.groups.share(7);
await h.groups.share(7, Array(5).fill('🐦'));
assert.equal(h.nodes.get('group-dialog').open, true);
assert.equal(h.nodes.get('group-entry-dialog').open, false);
assert.equal(h.nodes.get('group-code').children.length, 5);
@@ -209,14 +209,14 @@ test('sharing opens the actual five-symbol invitation directly without a two-sym
test('an incomplete invitation is never displayed as a two-symbol group code', async () => {
const h = await harness();
h.context.fetch = async () => ({ ok: true, json: async () => ({ group: { route: { id: 7 } }, code: ['🐦', '🪨'] }) });
await assert.rejects(h.groups.share(7));
await assert.rejects(h.groups.share(7, Array(5).fill('🐦')));
assert.equal(h.nodes.get('group-dialog').open, false);
assert.equal(h.nodes.get('group-code').children.length, 0);
});
test('the open invitation refreshes its code automatically and shows a deferred renewal during station editing', async () => {
test('the open invitation shows its current expiry without creating a code', async () => {
const h = await harness();
await h.groups.share(7);
await h.groups.share(7, Array(5).fill('🐦'));
const original = h.context.fetch;
h.context.fetch = async (url, options) => {
if (!url.endsWith('/invitation')) return original(url, options);
@@ -224,14 +224,69 @@ test('the open invitation refreshes its code automatically and shows a deferred
busy: true, expired: true, expiresAt: 1800000000 }) };
};
await h.groups.refreshInvitation();
assert.match(h.nodes.get('group-status').textContent, /Station bearbeitet/);
assert.match(h.nodes.get('group-status').textContent, /abgelaufen/);
h.context.fetch = async () => ({ ok: true, json: async () => ({ group: { id: 'group-id', route: { id: 22 }, role: 'owner' },
busy: false, expired: false, expiresAt: 1800001800, code: Array(5).fill('🪨') }) });
busy: false, expired: false, expiresAt: 1800001800, code: Array(5).fill('🐦') }) });
await h.groups.refreshInvitation();
assert.equal(h.nodes.get('group-code').children.length, 5);
assert.match(h.nodes.get('group-code').children[0].src, /🪨/);
assert.match(h.nodes.get('group-code').children[0].src, /🐦/);
assert.equal(h.nodes.get('group-status').textContent, '');
h.nodes.get('group-dialog').close();
h.context.fetch = async () => { throw new Error('must not fetch a closed invitation'); };
await h.groups.refreshInvitation();
});
test('sharing first offers five random editable symbols, sends only the confirmed code and never opens the editor', async () => {
const h = await harness();
const sharing = h.groups.share(7);
assert.equal(h.nodes.get('group-entry-dialog').open, true);
assert.equal(h.calls.length, 0);
assert.equal(h.nodes.get('group-entry-title').textContent, 'Code auswählen');
const columns = h.nodes.get('group-wheels').children;
assert.equal(columns.length, 5);
for (const column of columns) {
const wheel = column.children[1];
assert.ok(Number(wheel.attributes.get('aria-valuenow')) >= 1);
assert.equal(wheel.scrollTop, (Number(wheel.attributes.get('aria-valuenow')) - 1) * 96);
key(wheel, 'Home');
}
pointer(columns[2].children[1].children[5]);
pointer(h.nodes.get('group-join'));
await sharing;
assert.deepEqual(JSON.parse(h.calls[0].body).code, ['🪵', '🪵', '🐦', '🪵', '🪵']);
assert.equal(h.nodes.get('group-dialog').open, true);
assert.deepEqual(h.edits, []);
});
test('cancelling code selection sends no request and a late double tap cannot join a group', async () => {
const h = await harness();
const sharing = h.groups.share(7);
pointer(h.nodes.get('group-entry-close'));
await sharing;
pointer(h.nodes.get('group-join'));
await tick();
assert.equal(h.calls.length, 0);
assert.deepEqual(h.edits, []);
});
test('the countdown decreases, reflects extended expiry and shows expiration without changing the code', async () => {
const h = await harness();
let clock = 1799996400000;
h.context.Date = class extends Date { static now() { return clock; } };
await h.groups.share(7, Array(5).fill('🐦'));
assert.match(h.nodes.get('group-expiry').textContent, /60 Min. 00 Sek./);
const timer = h.timers.find(timer => timer.delay === 1000);
clock += 45_000;
timer.callback();
assert.match(h.nodes.get('group-expiry').textContent, /59 Min. 15 Sek./);
const code = h.nodes.get('group-code').children.map(image => image.src);
h.context.fetch = async () => ({ ok: true, json: async () => ({ group: { id: 'group-id', route: { id: 22 } },
code: Array(5).fill('🐦'), expiresAt: clock / 1000 + 3600 }) });
await h.groups.refreshInvitation();
assert.match(h.nodes.get('group-expiry').textContent, /60 Min. 00 Sek./);
clock += 3600_000;
timer.callback();
assert.match(h.nodes.get('group-expiry').textContent, /abgelaufen/);
assert.deepEqual(h.nodes.get('group-code').children.map(image => image.src), code);
});