Changeset fd8faf5 in Klonkt for test/search-fediverse.test.js


Ignore:
Timestamp:
09/30/2026 07:27:05 AM (2 hours ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
19e28ce
Parents:
86cfa1a
Message:

Zoeken is één vlak: de balk bovenaan, de resultaten eronder, en @naam@server

Robins ontwerp van 30-9: de aparte resultatenpagina is niet meer nodig als het
vak onder de zoekbalk blijft staan. De balk en het vak eronder zijn samen het
hele zoeken, en het vak krijgt alle ruimte die er onder de balk is.

EEN VORM, TWEE INGANGEN. partials/search-surface.ejs is de balk plus het vak;
de overlay op elke pagina gebruikt hem, en /search?q=... ook. De resultaten
zelf staan in partials/search-results.ejs, en het live vak haalt precies die
render op (/search/results). De pagina bevat dus teken voor teken wat het vak
toont; een toets bewaakt dat, want twee renders lopen gegarandeerd uit de pas.
/search blijft bestaan zodat een gedeelde link en Enter zonder JavaScript op
hetzelfde beeld uitkomen.

/search/suggest (JSON, dat de browser tot een eigen lijstje bouwde, anders dan
de pagina) en /search/remote (alleen het adres) zijn weg; niets anders
gebruikte ze. remote-preview.ejs ging mee.

HET VLAK. Vaste positie over het hele scherm, de balk bovenaan, het vak vult
de rest en scrolt zelf. Op z-index 1060: de vorige overlay lag op 200, onder de
tabbalk (1050), zodat op een telefoon het onderste stuk resultaten wegviel. Enter
blijft staan en zoekt meteen, want het vak ís de resultaten. Op de pagina beweegt
het adres mee (/search?q=...), zodat wat je ziet te delen blijft; sluiten is daar
teruggaan. Balk en resultaten staan op één lijn: een scrollbalk alleen rechts
schoof de kolom anders zo'n 9 pixels naar links.

WEBFINGER. @naam@server (met of zonder @ vooraan, zoals Mastodon) haalt het
profiel op, met de volgknop. Zonder eerst een post te proberen: een handle is
nooit een post. De server moet een echte domeinnaam zijn; "@robin" blijft een
naam om naar te zoeken. WebFinger zelf is openbaar en loopt via safeFetch;
het profiel erachter gaat ondertekend, en alleen namens een ingelogde
beheerder, dezelfde grens als een geplakte link.

Onderweg gevonden: search-results.ejs verwees kaal naar queryError, en een
aanroeper die hem vergat liet de hele render omvallen. Alles gaat nu via
locals. In de app gaf gather hem altijd mee, dus live ging het goed; het was
één vergeten veld van een kapotte zoekpagina.

Nagelopen in de browser op 375 en 1280 pixels: de balk bovenaan, het vak tot
de onderrand, tabbalk bedekt, geen horizontale overloop, tikdoelen 44, balk en
resultaten op één lijn. Het gedrag met de echte chrome.js: openen vers en met
focus, typen, Enter zonder weg te gaan, de wachtregel bij een link en een
handle, een traag antwoord dat niets overschrijft, een resultaat aantikken en
Escape sluiten, en het adres dat op de pagina meebeweegt.

Getoetst: pagina en vak teken voor teken gelijk, de handle-herkenning, een
handle ondertekend en zonder post-poging, en niet ingelogd geen enkel verzoek.
Tegenproef: pagina laten afwijken, handle niet herkennen, bij een handle toch
een post proberen en de inlogcontrole eruit laten elk hun toetsen omvallen.
Volledige suite 1292 groen.

shaer-utpi

Co-Authored-By: Claude Opus 5.5 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • test/search-fediverse.test.js

    r86cfa1a rfd8faf5  
    2626const searchMod = await import('../src/routes/search.js');
    2727const router = searchMod.default;
    28 const { lookupUri } = searchMod;
     28const { lookupUri, lookupHandle } = searchMod;
    2929
    3030db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
    … …  
    5555  gevraagd.push(['actor', uri]);
    5656  actorOpties.push(opts || {});
    57   if (!uri.includes('/u/')) return null;
     57  const actorUri = uri === '@oma@elders.test' ? 'https://elders.test/u/oma' : uri;
     58  if (!actorUri.includes('/u/')) return null;
    5859  return {
    59     actor_uri: uri, actor_name: 'Oma', actor_handle: '@oma@elders.test',
    60     actor_url: uri, actor_icon: 'https://elders.test/avatar.png', inbox: uri + '/inbox',
     60    actor_uri: actorUri, actor_name: 'Oma', actor_handle: '@oma@elders.test',
     61    actor_url: actorUri, actor_icon: 'https://elders.test/avatar.png', inbox: actorUri + '/inbox',
    6162  };
    6263};
    … …  
    105106  assert.equal(status, 200);
    106107  assert.deepEqual(gevraagd, [], 'een zoekterm hoort geen netwerkverzoek te worden');
    107   assert.ok(!html.includes('search-remote'), 'en er hoort geen fediverse-blok te staan');
     108  assert.ok(!html.includes('sr-fediverse'), 'en er hoort geen fediverse-blok te staan');
    108109});
    109110
    … …  
    113114  assert.ok(html.includes('Een bericht van ver weg'), 'de inhoud hoort er te staan');
    114115  assert.ok(html.includes('@oma@elders.test'), 'en van wie het is');
    115   // De functies van authorize_interaction, op de zoekpagina.
    116   for (const actie of ['/authorize_interaction/like', '/authorize_interaction/boost', '/authorize_interaction/report']) {
     116  // Waarderen en boosten kan ter plekke; antwoorden en melden op de volle
     117  // interactiepagina. De zoekpagina ziet er sinds 30-9 precies zo uit als het
     118  // live vak, dus ook hier de compacte kaart.
     119  for (const actie of ['/authorize_interaction/like', '/authorize_interaction/boost']) {
    117120    assert.ok(html.includes(`action="${actie}"`), `${actie} ontbreekt`);
    118121  }
    119   assert.ok(html.includes('action="/authorize_interaction"'), 'het antwoordvenster ontbreekt');
     122  assert.ok(html.includes('/authorize_interaction?uri=https%3A%2F%2Felders.test%2Fnotes%2F1&amp;reply=1'),
     123    'antwoorden wijst naar de volle pagina, met het venster open');
    120124});
    121125
    … …  
    131135  assert.equal(status, 200);
    132136  assert.equal(gevraagd.length, 2, 'beide vormen zijn geprobeerd');
    133   assert.ok(html.includes('search-remote'), 'het blok staat er, met de melding erin');
    134   assert.ok(html.includes('search-page-form'), 'en de zoekpagina zelf is niet weggevallen');
     137  assert.ok(/sr-fediverse[\s\S]*class="sr-note"/.test(html), 'het blok staat er, met de melding erin');
     138  assert.ok(html.includes('search-bar-form'), 'en de zoekbalk zelf is niet weggevallen');
    135139});
    136140
    … …  
    203207const preview = async (q) => {
    204208  gevraagd = []; opties = [];
    205   const r = await fetch(`http://127.0.0.1:${poort}/search/remote?q=${encodeURIComponent(q)}`, { signal: AbortSignal.timeout(10000) });
     209  const r = await fetch(`http://127.0.0.1:${poort}/search/results?q=${encodeURIComponent(q)}`, { signal: AbortSignal.timeout(10000) });
    206210  return { status: r.status, html: await r.text(), cache: r.headers.get('cache-control') };
    207211};
    … …  
    240244  try {
    241245    const { status, html } = await preview('https://elders.test/notes/1');
    242     // 204 en geen 403: voor een bezoeker is een geplakte link gewoon een
    243     // zoekterm, en de zoekbalk valt dan terug op de gewone suggesties.
    244     assert.equal(status, 204);
     246    // Voor een bezoeker is een geplakte link gewoon een zoekterm: hij krijgt
     247    // de gewone resultaten en de zin waarom er verder niets staat.
     248    assert.equal(status, 200);
    245249    assert.deepEqual(gevraagd, [], 'zonder rechten hoort er geen enkel verzoek uit te gaan');
    246     assert.equal(html, '');
     250    assert.ok(!html.includes('/authorize_interaction/like'), 'en er staan geen knoppen');
    247251  } finally {
    248252    ingelogd = true;
    … …  
    252256test('een zoekterm in de preview haalt niets op', async () => {
    253257  const { status } = await preview('soundfabrics.nl');
    254   assert.equal(status, 204);
    255   assert.deepEqual(gevraagd, []);
     258  assert.equal(status, 200, 'gewoon zoeken');
     259  assert.deepEqual(gevraagd, [], 'maar niets ophalen');
    256260});
    257261
    … …  
    323327});
    324328
     329// ── Eén zoekvlak: de pagina en het live vak zijn hetzelfde ─────────────────
     330//
     331// Robins ontwerp van 30-9: de zoekbalk en het vak eronder zijn het hele
     332// zoeken, en /search?q=... ziet er hetzelfde uit. Dat is alleen waar zolang
     333// beide dezelfde render gebruiken; deze toets vangt het moment dat ze uit
     334// elkaar gaan lopen.
     335
     336test('de pagina en het live vak tonen dezelfde resultaten, teken voor teken', async () => {
     337  const pagina = (await zoek('https://elders.test/notes/1')).html;
     338  const vak = (await preview('https://elders.test/notes/1')).html.trim();
     339  assert.ok(vak.startsWith('<div class="sr">'), 'het vak levert de resultaten-render');
     340  assert.ok(pagina.includes(vak), 'de pagina bevat exact dezelfde render als het live vak');
     341});
     342
     343test('een lege vraag in het live vak levert niets en haalt niets op', async () => {
     344  const { status } = await preview('x');
     345  assert.equal(status, 204);
     346  assert.deepEqual(gevraagd, []);
     347});
     348
     349// ── WebFinger: @naam@server in de zoekbalk ───────────────────────────────────
     350
     351test('een handle wordt herkend, een naam of zin niet', () => {
     352  assert.equal(lookupHandle('@oma@elders.test'), '@oma@elders.test');
     353  assert.equal(lookupHandle('oma@Elders.Test'), '@oma@elders.test', 'zonder @ vooraan, zoals Mastodon');
     354  assert.equal(lookupHandle('  @oma.v_d-berg@sub.elders.test  '), '@oma.v_d-berg@sub.elders.test');
     355  assert.equal(lookupHandle('@oma'), null, 'een naam om naar te zoeken, geen adres');
     356  assert.equal(lookupHandle('@oma@localhost'), null, 'een server zonder domeinnaam');
     357  assert.equal(lookupHandle('oma @elders.test'), null, 'een spatie maakt er een zoekopdracht van');
     358  assert.equal(lookupHandle('https://elders.test/@oma'), null, 'een adres is een adres, geen handle');
     359});
     360
     361test('een handle haalt het profiel op, ondertekend, zonder eerst een post te proberen', async () => {
     362  actorOpties = [];
     363  const { html } = await preview('@oma@elders.test');
     364  assert.deepEqual(gevraagd, [['actor', '@oma@elders.test']], 'alleen het profiel: een handle is nooit een post');
     365  assert.equal(actorOpties[0] && actorOpties[0].asSlug, 'robo', 'het profiel achter de handle gaat ondertekend');
     366  assert.ok(html.includes('action="/authorize_interaction/follow"'), 'met de volgknop');
     367  assert.ok(html.includes('https://elders.test/u/oma'), 'voor het profiel dat WebFinger opleverde');
     368});
     369
     370test('een handle op de zoekpagina geeft hetzelfde profiel', async () => {
     371  const { html } = await zoek('oma@elders.test');
     372  assert.ok(html.includes('action="/authorize_interaction/follow"'));
     373});
     374
     375test('niet ingelogd levert een handle geen enkel verzoek op', async () => {
     376  ingelogd = false;
     377  try {
     378    const { status, html } = await preview('@oma@elders.test');
     379    assert.equal(status, 200);
     380    assert.deepEqual(gevraagd, [], 'geen WebFinger en geen profiel');
     381    assert.ok(!html.includes('/authorize_interaction/follow'));
     382  } finally {
     383    ingelogd = true;
     384  }
     385});
     386
Note: See TracChangeset for help on using the changeset viewer.