Fullfører ADR-063 -- backend var allerede committet (206beef). Bruker
ba eksplisitt om at Claude selv implementerer frontend-promptet siden
V0-kreditter var oppbrukt.
lib/mentions.ts: ren offset-/diff-logikk (deteksjon, innsetting,
diff-justering ved redigering, tekst-segmentering), 15 enhetstester.
components/mention-input.tsx: MentionTextarea (autocomplete-dropdown,
tastaturnavigasjon) + TaggedText (lenker til /my-friends/{id}). Koblet
inn i round-messages.tsx (innlegg) og post-engagement.tsx (kommentarer,
delt av flere meldingssystemer -- roundId/tags gjort valgfrie for å
ikke bryte de org-scopede callerne uten tagging).
Rettet også et hull oppdaget underveis: backendens GET /feed manglet
tags (kun list_round_messages/comments hadde det) -- det er nettopp
/my-feed-siden som var det opprinnelige brukseksempelet.
Scratch-verifisert (egen DB/rolle/MinIO/API-/frontend-container, to
ekte testbrukere, venn-relasjon, felles runde): autocomplete, tagging
i både innlegg og kommentar, rendring som lenke i rundevisning OG i
aggregert feed, varsel opprettet, personvern-scoping bekreftet (kun
faktiske relasjoner tilbys, ikke seg selv, ikke fremmede), lys+mørk.
tsc --noEmit rent, 45/45 vitest grønt. teecup_db urørt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
121 lines
5.5 KiB
TypeScript
121 lines
5.5 KiB
TypeScript
import { describe, expect, it } from "vitest"
|
|
import { adjustTagsForEdit, detectActiveMention, insertMention, segmentTaggedText } from "./mentions"
|
|
|
|
describe("detectActiveMention", () => {
|
|
it("finner @-forsøk ved starten av teksten", () => {
|
|
expect(detectActiveMention("@Fre", 4)).toEqual({ start: 0, query: "Fre" })
|
|
})
|
|
|
|
it("finner @-forsøk midt i teksten, etter mellomrom", () => {
|
|
const body = "God runde med @Fre"
|
|
expect(detectActiveMention(body, body.length)).toEqual({ start: 14, query: "Fre" })
|
|
})
|
|
|
|
it("trigges IKKE av en e-post-adresse (@ uten forutgående mellomrom)", () => {
|
|
const body = "kontakt meg på navn@example"
|
|
expect(detectActiveMention(body, body.length)).toBeNull()
|
|
})
|
|
|
|
it("returnerer null når markøren ikke er i et @-forsøk i det hele tatt", () => {
|
|
expect(detectActiveMention("bare vanlig tekst", 5)).toBeNull()
|
|
})
|
|
|
|
it("returnerer null når @-forsøket ble avbrutt av et mellomrom (fullført/forlatt)", () => {
|
|
const body = "@Fredrik er kul"
|
|
expect(detectActiveMention(body, body.length)).toBeNull()
|
|
})
|
|
|
|
it("virker med markøren midt i ordet, ikke bare på slutten", () => {
|
|
const body = "@Fredrik Mathisen"
|
|
// markør rett etter "Fre" i "Fredrik"
|
|
expect(detectActiveMention(body, 4)).toEqual({ start: 0, query: "Fre" })
|
|
})
|
|
})
|
|
|
|
describe("insertMention", () => {
|
|
it("erstatter @søketekst med @Fullt Navn og regner riktig ny markørposisjon", () => {
|
|
const body = "God runde med @Fre i dag"
|
|
const start = 14
|
|
const cursor = 18 // rett etter "Fre"
|
|
const result = insertMention(body, start, cursor, { user_id: "u1", display_name: "Fredrik Mathisen" })
|
|
expect(result.body).toBe("God runde med @Fredrik Mathisen i dag")
|
|
expect(result.tag).toEqual({ user_id: "u1", display_name: "Fredrik Mathisen", start_index: 14, end_index: 31 })
|
|
expect(result.cursor).toBe(31)
|
|
// Taggens eget utsnitt skal faktisk væra "@Fredrik Mathisen".
|
|
expect(result.body.slice(result.tag.start_index, result.tag.end_index)).toBe("@Fredrik Mathisen")
|
|
})
|
|
})
|
|
|
|
describe("adjustTagsForEdit", () => {
|
|
const tag = { user_id: "u1", display_name: "Fredrik Mathisen", start_index: 14, end_index: 32 }
|
|
|
|
it("beholder taggen uendret når redigeringen skjer ETTER den", () => {
|
|
const oldBody = "God runde med @Fredrik Mathisen i dag"
|
|
const newBody = "God runde med @Fredrik Mathisen i dag, supert vær"
|
|
const result = adjustTagsForEdit(oldBody, newBody, [tag])
|
|
expect(result).toEqual([tag])
|
|
})
|
|
|
|
it("forskyver taggen når tekst settes inn FØR den", () => {
|
|
const oldBody = "med @Fredrik Mathisen i dag"
|
|
const newBody = "God runde med @Fredrik Mathisen i dag"
|
|
const shifted = { ...tag, start_index: 0, end_index: 18 } // relativt til oldBody sin tag
|
|
const oldTag = { ...tag, start_index: 4, end_index: 22 }
|
|
const result = adjustTagsForEdit(oldBody, newBody, [oldTag])
|
|
const delta = newBody.length - oldBody.length
|
|
expect(result).toEqual([{ ...oldTag, start_index: oldTag.start_index + delta, end_index: oldTag.end_index + delta }])
|
|
})
|
|
|
|
it("dropper taggen stille når redigeringen overlapper dens eget utsnitt", () => {
|
|
const oldBody = "med @Fredrik Mathisen i dag"
|
|
const oldTag = { ...tag, start_index: 4, end_index: 22 }
|
|
// Sletter en bokstav MIDT i det taggede navnet.
|
|
const newBody = "med @Fredrik athisen i dag"
|
|
const result = adjustTagsForEdit(oldBody, newBody, [oldTag])
|
|
expect(result).toEqual([])
|
|
})
|
|
|
|
it("dropper KUN den berørte taggen, beholder en urørt tag et annet sted", () => {
|
|
const oldBody = "@Fredrik og @Kari spilte i dag"
|
|
const fredrikTag = { user_id: "u1", display_name: "Fredrik", start_index: 0, end_index: 8 }
|
|
const kariTag = { user_id: "u2", display_name: "Kari", start_index: 12, end_index: 17 }
|
|
// Redigerer midt i "Fredrik".
|
|
const newBody = "@Fred og @Kari spilte i dag"
|
|
const result = adjustTagsForEdit(oldBody, newBody, [fredrikTag, kariTag])
|
|
const delta = newBody.length - oldBody.length
|
|
expect(result).toEqual([{ ...kariTag, start_index: kariTag.start_index + delta, end_index: kariTag.end_index + delta }])
|
|
})
|
|
})
|
|
|
|
describe("segmentTaggedText", () => {
|
|
it("deler teksten korrekt rundt én tag", () => {
|
|
const body = "God runde med @Fredrik Mathisen i dag"
|
|
const tag = { user_id: "u1", display_name: "Fredrik Mathisen", start_index: 14, end_index: 31 }
|
|
const segments = segmentTaggedText(body, [tag])
|
|
expect(segments).toEqual([
|
|
{ text: "God runde med ", tag: null },
|
|
{ text: "@Fredrik Mathisen", tag },
|
|
{ text: " i dag", tag: null },
|
|
])
|
|
})
|
|
|
|
it("returnerer hele teksten som ett segment når det ikke finnes tags", () => {
|
|
const segments = segmentTaggedText("bare vanlig tekst", [])
|
|
expect(segments).toEqual([{ text: "bare vanlig tekst", tag: null }])
|
|
})
|
|
|
|
it("håndterer flere tags i rekkefølge", () => {
|
|
const body = "@Fredrik og @Kari spilte"
|
|
const t1 = { user_id: "u1", display_name: "Fredrik", start_index: 0, end_index: 8 }
|
|
const t2 = { user_id: "u2", display_name: "Kari", start_index: 12, end_index: 17 }
|
|
const segments = segmentTaggedText(body, [t1, t2])
|
|
expect(segments.map((s) => s.text)).toEqual(["@Fredrik", " og ", "@Kari", " spilte"])
|
|
})
|
|
|
|
it("hopper defensivt over en tag med ugyldig/utenfor-grense offset i stedet for å krasje", () => {
|
|
const body = "kort tekst"
|
|
const badTag = { user_id: "u1", display_name: "X", start_index: 5, end_index: 999 }
|
|
expect(() => segmentTaggedText(body, [badTag])).not.toThrow()
|
|
expect(segmentTaggedText(body, [badTag])).toEqual([{ text: "kort tekst", tag: null }])
|
|
})
|
|
})
|