oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1package docs_test
2
3import (
4 "context"
5 "regexp"
6 "strings"
7 "testing"
8
9 "github.com/carlosframework/docs/internal/docs"
10)
11
12// tagPattern strips element markup so TestSeedContentMatchesItsSource
13// can hold the *prose* honest, not the exact tags — the block
14// classifier in fixtures/generate-seed.mjs is free to change which
15// element a given block becomes without this test caring.
16var tagPattern = regexp.MustCompile(`<[^>]*>`)
17
18// TestSeedProducesReadableDocuments is the task brief's own test,
19// adapted for this task's own move: Seed must leave at least four
20// documents behind, and every one of them must carry substantial
21// content read back through the real CRDT — as of this task, through
22// Store.HTML, not Store.Text. The seed generator now builds a
23// Y.XmlFragment on root "prosemirror" (fixtures/generate-seed.mjs),
24// not a Y.Text on root "content", so Store.Text legitimately reads
25// back empty for a seeded document; HTML is the read path that
26// actually replays what Seed wrote.
27func TestSeedProducesReadableDocuments(t *testing.T) {
28 app, _ := docs.NewForTest(t)
29 ctx := context.Background()
30 owner := app.TestPerson(t, "seed@example.com")
31
32 if err := docs.Seed(ctx, app.Store(), app.TestActorForPerson(t, owner)); err != nil {
33 t.Fatalf("Seed: %v", err)
34 }
35
36 list, err := app.Store().List(ctx, owner)
37 if err != nil {
38 t.Fatal(err)
39 }
40 if len(list) < 4 {
41 t.Fatalf("seeded %d documents, want at least 4", len(list))
42 }
43
44 // Every seeded document must have real content, through the CRDT.
45 for _, d := range list {
46 html, err := app.Store().HTML(ctx, d)
47 if err != nil {
48 t.Fatalf("HTML(%q): %v", d.Title, err)
49 }
50 if len(html) < 200 {
51 t.Errorf("%q has %d characters of HTML — seed content should be substantial", d.Title, len(html))
52 }
53 }
54}
55
56// TestSeedVariesTimestamps pins the plan's own requirement that
57// seeded documents do not all land in the same second: List's own
58// list.html groups by recency (handlers.go's recencyBucket), and every
59// document created in the same second shows nothing of that. This
60// does not assert exact values — Seed's own Ago durations are an
61// implementation detail free to change — only that CreatedAt is not
62// identical across every document, which a naive "loop and Create"
63// seeder (no backdating at all) would fail.
64func TestSeedVariesTimestamps(t *testing.T) {
65 app, _ := docs.NewForTest(t)
66 ctx := context.Background()
67 owner := app.TestPerson(t, "seed@example.com")
68
69 if err := docs.Seed(ctx, app.Store(), app.TestActorForPerson(t, owner)); err != nil {
70 t.Fatalf("Seed: %v", err)
71 }
72
73 list, err := app.Store().List(ctx, owner)
74 if err != nil {
75 t.Fatal(err)
76 }
77
78 seen := map[string]bool{}
79 for _, d := range list {
80 seen[d.CreatedAt.Format("2006-01-02T15:04")] = true
81 }
82 if len(seen) < 2 {
83 t.Fatalf("all %d seeded documents share the same CreatedAt minute — timestamps are not varied", len(list))
84 }
85}
86
87// TestSeedContentMatchesItsSource holds seed.go's committed
88// .update.bin fixtures honest against the .txt files they were built
89// from (fixtures/generate-seed.mjs): if a .txt is edited without
90// regenerating its .bin, the document Store.HTML() actually replays
91// stops matching what a person editing seeddata/ would expect to see,
92// silently — this test is what makes that loud instead. It is the Go
93// side of the same contract the wire-compatibility gate's own
94// fixtures (fixtures/generate.mjs, scripts/wire-gate.sh) hold for
95// fixtures/data.
96//
97// This checked Store.Text() before this task: the seed generator now
98// builds a Y.XmlFragment on root "prosemirror", not a Y.Text on root
99// "content" (generate-seed.mjs's own comment), so Text legitimately
100// reads back empty for a seeded document — HTML is the read path
101// that actually replays what Seed wrote. Tags are stripped before the
102// prose checks below so this test holds the content honest, not the
103// exact element the block classifier chose for a given block.
104func TestSeedContentMatchesItsSource(t *testing.T) {
105 app, _ := docs.NewForTest(t)
106 ctx := context.Background()
107 owner := app.TestPerson(t, "source-check@example.com")
108
109 if err := docs.Seed(ctx, app.Store(), app.TestActorForPerson(t, owner)); err != nil {
110 t.Fatalf("Seed: %v", err)
111 }
112
113 list, err := app.Store().List(ctx, owner)
114 if err != nil {
115 t.Fatal(err)
116 }
117
118 titleSeen := map[string]bool{}
119 for _, d := range list {
120 titleSeen[d.Title] = true
121 }
122
123 for _, want := range []string{
124 "Proposal: Refitting the Elm Street Reading Room",
125 "Notes — Tuesday Planning Sync",
126 "Quick Reference: Resetting the Site Printer",
127 "Letter to the Harbourview Allotment Committee",
128 "A History of the Millbrook Waterworks",
129 "Packing List — Thursday Site Visit",
130 } {
131 if !titleSeen[want] {
132 t.Errorf("seeded set is missing %q", want)
133 }
134 }
135
136 for _, d := range list {
137 html, err := app.Store().HTML(ctx, d)
138 if err != nil {
139 t.Fatalf("HTML(%q): %v", d.Title, err)
140 }
141 if !strings.Contains(string(html), "<p>") {
142 t.Errorf("%q rendered no paragraphs: %.120q", d.Title, html)
143 }
144 text := strings.TrimSpace(tagPattern.ReplaceAllString(string(html), " "))
145 if text == "" {
146 t.Errorf("%q replayed to empty text", d.Title)
147 }
148 // A loose but real cross-check: the replayed text should read
149 // as prose (contains at least one space-separated sentence),
150 // not e.g. a stray binary artefact from a stale .bin.
151 if !strings.Contains(text, " ") {
152 t.Errorf("%q replayed to %q, which does not look like prose", d.Title, text)
153 }
154 }
155}
156
157// TestSeededDocumentsRenderAsHTML holds the seeded documents to the
158// ProseMirror-fragment round trip, not merely the plain-text one
159// TestSeedContentMatchesItsSource already covers: the seed generator
160// (fixtures/generate-seed.mjs) now builds a Y.XmlFragment, and
161// Store.HTML reads it back through core's fragment renderer
162// (core/src/fragment.rs). A fixture that applies cleanly but renders
163// to nothing — the empty-fragment failure Task 1 introduced for the
164// old Y.Text-only seed data — is the failure this test exists to
165// catch.
166func TestSeededDocumentsRenderAsHTML(t *testing.T) {
167 app, err := docs.NewForTest(t)
168 if err != nil {
169 t.Fatalf("NewForTest: %v", err)
170 }
171 ctx := context.Background()
172 owner := app.TestPerson(t, "render@example.com")
173 if err := docs.Seed(ctx, app.Store(), app.TestActorForPerson(t, owner)); err != nil {
174 t.Fatalf("Seed: %v", err)
175 }
176 docList, err := app.Store().List(ctx, owner)
177 if err != nil {
178 t.Fatalf("List: %v", err)
179 }
180 for _, d := range docList {
181 html, err := app.Store().HTML(ctx, d)
182 if err != nil {
183 t.Fatalf("HTML(%q): %v", d.Title, err)
184 }
185 if !strings.Contains(string(html), "<p>") {
186 t.Errorf("%q rendered no paragraphs: %.120q", d.Title, html)
187 }
188 }
189}
190
191// TestWaterworksHistoryHasHeadings pins generate-seed.mjs's own block
192// classifier: a short line with no trailing full stop, followed by a
193// blank line, becomes a level-2 heading rather than an ordinary
194// paragraph. "A History of the Millbrook Waterworks" is the one seed
195// document long enough to have real section breaks ("Early years",
196// "Construction and the first reservoir", ...) — if this renders no
197// <h2>, the classifier stopped doing its job.
198func TestWaterworksHistoryHasHeadings(t *testing.T) {
199 app, err := docs.NewForTest(t)
200 if err != nil {
201 t.Fatalf("NewForTest: %v", err)
202 }
203 ctx := context.Background()
204 owner := app.TestPerson(t, "headings@example.com")
205 if err := docs.Seed(ctx, app.Store(), app.TestActorForPerson(t, owner)); err != nil {
206 t.Fatalf("Seed: %v", err)
207 }
208 docList, _ := app.Store().List(ctx, owner)
209 for _, d := range docList {
210 if !strings.Contains(d.Title, "Millbrook") {
211 continue
212 }
213 html, err := app.Store().HTML(ctx, d)
214 if err != nil {
215 t.Fatalf("HTML: %v", err)
216 }
217 if !strings.Contains(string(html), "<h2>") {
218 t.Error("the long history rendered no headings — the block " +
219 "classifier in generate-seed.mjs is not doing its job")
220 }
221 return
222 }
223 t.Fatal("the Millbrook waterworks document is missing from the seed set")
224}
225
226// TestSeedTextReturnsRealProse pins the plain-text accessor's own
227// contract for a real document: Store.Text must return the seeded
228// document's actual prose, not silently read back "".
229//
230// Fix round 1 on this task: Store.Text existed with zero production
231// callers and read CRDT root "content" (a bare Y.Text) — the shape
232// every real document left behind the moment seed data (and, later,
233// the editor) moved to root "prosemirror" (a Y.XmlFragment, Task 1).
234// Nothing caught that Text had quietly stopped meaning anything for a
235// real document: it returned "" with no error, the exact silent-empty
236// failure shape this codebase keeps producing. This test is the
237// assertion that would have caught it — it must pass against
238// core/src/abi.rs's text_string once that reads fragment_text instead
239// of text_content, and it must fail before that change lands.
240func TestSeedTextReturnsRealProse(t *testing.T) {
241 app, err := docs.NewForTest(t)
242 if err != nil {
243 t.Fatalf("NewForTest: %v", err)
244 }
245 ctx := context.Background()
246 owner := app.TestPerson(t, "text-prose@example.com")
247 if err := docs.Seed(ctx, app.Store(), app.TestActorForPerson(t, owner)); err != nil {
248 t.Fatalf("Seed: %v", err)
249 }
250 docList, err := app.Store().List(ctx, owner)
251 if err != nil {
252 t.Fatalf("List: %v", err)
253 }
254 for _, d := range docList {
255 if !strings.Contains(d.Title, "Packing List") {
256 continue
257 }
258 text, err := app.Store().Text(ctx, d)
259 if err != nil {
260 t.Fatalf("Text: %v", err)
261 }
262 if !strings.Contains(text, "Leaving from the depot at 7:30") {
263 t.Errorf("Text = %.200q, want it to contain the packing list's real opening line", text)
264 }
265 return
266 }
267 t.Fatal("the packing list document is missing from the seed set")
268}
269