1 package dark_theme_test
2
3 import (
4 "context"
5 "encoding/xml"
6 "io/ioutil"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11
12 "cdr.dev/slog"
13
14 tassert "github.com/stretchr/testify/assert"
15
16 "oss.terrastruct.com/util-go/assert"
17 "oss.terrastruct.com/util-go/diff"
18 "oss.terrastruct.com/util-go/go2"
19
20 "oss.terrastruct.com/d2/d2graph"
21 "oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
22 "oss.terrastruct.com/d2/d2lib"
23 "oss.terrastruct.com/d2/d2renderers/d2fonts"
24 "oss.terrastruct.com/d2/d2renderers/d2svg"
25 "oss.terrastruct.com/d2/lib/log"
26 "oss.terrastruct.com/d2/lib/textmeasure"
27 )
28
29 func TestDarkTheme(t *testing.T) {
30 t.Parallel()
31
32 tcs := []testCase{
33 {
34 name: "basic",
35 script: `a -> b
36 `,
37 },
38 {
39 name: "child to child",
40 script: `winter.snow -> summer.sun
41 `,
42 },
43 {
44 name: "animated",
45 script: `winter.snow -> summer.sun -> trees -> winter.snow: { style.animated: true }
46 `,
47 },
48 {
49 name: "connection label",
50 script: `a -> b: hello
51 `,
52 },
53 {
54 name: "twitter",
55 script: `timeline mixer: "" {
56 explanation: |md
57 ## **Timeline mixer**
58 - Inject ads, who-to-follow, onboarding
59 - Conversation module
60 - Cursoring,pagination
61 - Tweat deduplication
62 - Served data logging
63 |
64 }
65 People discovery: "People discovery \nservice"
66 admixer: Ad mixer {
67 style.fill: "#cba6f7"
68 style.font-color: "#000000"
69 }
70
71 onboarding service: "Onboarding \nservice"
72 timeline mixer -> People discovery
73 timeline mixer -> onboarding service
74 timeline mixer -> admixer
75 container0: "" {
76 graphql
77 comment
78 tlsapi
79 }
80 container0.graphql: GraphQL\nFederated Strato Column {
81 shape: image
82 icon: https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/GraphQL_Logo.svg/1200px-GraphQL_Logo.svg.png
83 }
84 container0.comment: |md
85 ## Tweet/user content hydration, visibility filtering
86 |
87 container0.tlsapi: TLS-API (being deprecated)
88 container0.graphql -> timeline mixer
89 timeline mixer <- container0.tlsapi
90 twitter fe: "Twitter Frontend " {
91 icon: https://icons.terrastruct.com/social/013-twitter-1.svg
92 shape: image
93 }
94 twitter fe -> container0.graphql: iPhone web
95 twitter fe -> container0.tlsapi: HTTP Android
96 web: Web {
97 icon: https://icons.terrastruct.com/azure/Web%20Service%20Color/App%20Service%20Domains.svg
98 shape: image
99 }
100
101 Iphone: {
102 icon: 'https://ss7.vzw.com/is/image/VerizonWireless/apple-iphone-12-64gb-purple-53017-mjn13ll-a?$device-lg$'
103 shape: image
104 }
105 Android: {
106 icon: https://cdn4.iconfinder.com/data/icons/smart-phones-technologies/512/android-phone.png
107 shape: image
108 }
109
110 web -> twitter fe
111 timeline scorer: "Timeline\nScorer" {
112 style.fill: "#fab387"
113 style.font-color: "#000000"
114 }
115 home ranker: Home Ranker
116
117 timeline service: Timeline Service
118 timeline mixer -> timeline scorer: Thrift RPC
119 timeline mixer -> home ranker: {
120 style.stroke-dash: 4
121 style.stroke: "#000E3D"
122 }
123 timeline mixer -> timeline service
124 home mixer: Home mixer {
125 # style.fill: "#c1a2f3"
126 }
127 container0.graphql -> home mixer: {
128 style.stroke-dash: 4
129 style.stroke: "#000E3D"
130 }
131 home mixer -> timeline scorer
132 home mixer -> home ranker: {
133 style.stroke-dash: 4
134 style.stroke: "#000E3D"
135 }
136 home mixer -> timeline service
137 manhattan 2: Manhattan
138 gizmoduck: Gizmoduck
139 socialgraph: Social graph
140 tweetypie: Tweety Pie
141 home mixer -> manhattan 2
142 home mixer -> gizmoduck
143 home mixer -> socialgraph
144 home mixer -> tweetypie
145 Iphone -> twitter fe
146 Android -> twitter fe
147 prediction service2: Prediction Service {
148 shape: image
149 icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
150 }
151 home scorer: Home Scorer {
152 style.fill: "#eba0ac"
153 style.font-color: "#000000"
154 }
155 manhattan: Manhattan
156 memcache: Memcache {
157 icon: https://d1q6f0aelx0por.cloudfront.net/product-logos/de041504-0ddb-43f6-b89e-fe04403cca8d-memcached.png
158 }
159
160 fetch: Fetch {
161 style.multiple: true
162 shape: step
163 }
164 feature: Feature {
165 style.multiple: true
166 shape: step
167 }
168 scoring: Scoring {
169 style.multiple: true
170 shape: step
171 }
172 fetch -> feature
173 feature -> scoring
174
175 prediction service: Prediction Service {
176 shape: image
177 icon: https://cdn-icons-png.flaticon.com/512/6461/6461819.png
178 }
179 scoring -> prediction service
180 fetch -> container2.crmixer
181
182 home scorer -> manhattan: ""
183
184 home scorer -> memcache: ""
185 home scorer -> prediction service2
186 home ranker -> home scorer
187 home ranker -> container2.crmixer: Candidate Fetch
188 container2: "" {
189 style.stroke: "#b4befe"
190 style.fill: "#000000"
191 crmixer: CrMixer {
192 style.fill: "#11111b"
193 style.font-color: "#cdd6f4"
194 }
195 earlybird: EarlyBird
196 utag: Utag
197 space: Space
198 communities: Communities
199 }
200 etc: ...etc
201
202 home scorer -> etc: Feature Hydration
203
204 feature -> manhattan
205 feature -> memcache
206 feature -> etc: Candidate sources
207 `,
208 },
209 {
210 name: "all_shapes",
211 script: `
212 rectangle: {shape: "rectangle"}
213 square: {shape: "square"}
214 page: {shape: "page"}
215 parallelogram: {shape: "parallelogram"}
216 document: {shape: "document"}
217 cylinder: {shape: "cylinder"}
218 queue: {shape: "queue"}
219 package: {shape: "package"}
220 step: {shape: "step"}
221 callout: {shape: "callout"}
222 stored_data: {shape: "stored_data"}
223 person: {shape: "person"}
224 diamond: {shape: "diamond"}
225 oval: {shape: "oval"}
226 circle: {shape: "circle"}
227 hexagon: {shape: "hexagon"}
228 cloud: {shape: "cloud"}
229
230 rectangle -> square -> page
231 parallelogram -> document -> cylinder
232 queue -> package -> step
233 callout -> stored_data -> person
234 diamond -> oval -> circle
235 hexagon -> cloud
236 `,
237 },
238 {
239 name: "sql_tables",
240 script: `users: {
241 shape: sql_table
242 id: int
243 name: string
244 email: string
245 password: string
246 last_login: datetime
247 }
248
249 products: {
250 shape: sql_table
251 id: int
252 price: decimal
253 sku: string
254 name: string
255 }
256
257 orders: {
258 shape: sql_table
259 id: int
260 user_id: int
261 product_id: int
262 }
263
264 shipments: {
265 shape: sql_table
266 id: int
267 order_id: int
268 tracking_number: string {constraint: primary_key}
269 status: string
270 }
271
272 users.id <-> orders.user_id
273 products.id <-> orders.product_id
274 shipments.order_id <-> orders.id`,
275 },
276 {
277 name: "class",
278 script: `manager: BatchManager {
279 shape: class
280 -num: int
281 -timeout: int
282 -pid
283
284 +getStatus(): Enum
285 +getJobs(): "Job[]"
286 +setTimeout(seconds int)
287 }
288 `,
289 },
290 {
291 name: "arrowheads",
292 script: `
293 a: ""
294 b: ""
295 a.1 -- b.1: none
296 a.2 <-> b.2: arrow {
297 source-arrowhead.shape: arrow
298 target-arrowhead.shape: arrow
299 }
300 a.3 <-> b.3: triangle {
301 source-arrowhead.shape: triangle
302 target-arrowhead.shape: triangle
303 }
304 a.4 <-> b.4: diamond {
305 source-arrowhead.shape: diamond
306 target-arrowhead.shape: diamond
307 }
308 a.5 <-> b.5: diamond filled {
309 source-arrowhead: {
310 shape: diamond
311 style.filled: true
312 }
313 target-arrowhead: {
314 shape: diamond
315 style.filled: true
316 }
317 }
318 a.6 <-> b.6: cf-many {
319 source-arrowhead.shape: cf-many
320 target-arrowhead.shape: cf-many
321 }
322 a.7 <-> b.7: cf-many-required {
323 source-arrowhead.shape: cf-many-required
324 target-arrowhead.shape: cf-many-required
325 }
326 a.8 <-> b.8: cf-one {
327 source-arrowhead.shape: cf-one
328 target-arrowhead.shape: cf-one
329 }
330 a.9 <-> b.9: cf-one-required {
331 source-arrowhead.shape: cf-one-required
332 target-arrowhead.shape: cf-one-required
333 }
334 `,
335 },
336 {
337 name: "opacity",
338 script: `x.style.opacity: 0.4
339 y: |md
340 linux: because a PC is a terrible thing to waste
341 | {
342 style.opacity: 0.4
343 }
344 x -> a: {
345 label: You don't have to know how the computer works,\njust how to work the computer.
346 style.opacity: 0.4
347 }
348 users: {
349 shape: sql_table
350 last_login: datetime
351 style.opacity: 0.4
352 }
353 `,
354 },
355 {
356 name: "overlay",
357 script: `bright: {
358 style.stroke: "#000"
359 style.font-color: "#000"
360 style.fill: "#fff"
361 }
362 normal: {
363 style.stroke: "#000"
364 style.font-color: "#000"
365 style.fill: "#ccc"
366 }
367 dark: {
368 style.stroke: "#000"
369 style.font-color: "#fff"
370 style.fill: "#555"
371 }
372 darker: {
373 style.stroke: "#000"
374 style.font-color: "#fff"
375 style.fill: "#000"
376 }
377 `,
378 },
379 {
380 name: "code",
381 script: `code: |go
382 func main() {
383 panic("TODO")
384 }
385 |
386
387 text: |md
388 Five is a sufficiently close approximation to infinity.
389 |
390 unknown: |asdf
391 Don't hit me!! I'm in the Twilight Zone!!!
392 |
393 code -- unknown
394 `,
395 },
396 }
397 runa(t, tcs)
398 }
399
400 type testCase struct {
401 name string
402 script string
403 skip bool
404 }
405
406 func runa(t *testing.T, tcs []testCase) {
407 for _, tc := range tcs {
408 tc := tc
409 t.Run(tc.name, func(t *testing.T) {
410 if tc.skip {
411 t.Skip()
412 }
413 t.Parallel()
414
415 run(t, tc)
416 })
417 }
418 }
419
420 func run(t *testing.T, tc testCase) {
421 ctx := context.Background()
422 ctx = log.WithTB(ctx, t, nil)
423 ctx = log.Leveled(ctx, slog.LevelDebug)
424
425 ruler, err := textmeasure.NewRuler()
426 if !tassert.Nil(t, err) {
427 return
428 }
429
430 renderOpts := &d2svg.RenderOpts{
431 ThemeID: go2.Pointer(int64(200)),
432 }
433 layoutResolver := func(engine string) (d2graph.LayoutGraph, error) {
434 return d2dagrelayout.DefaultLayout, nil
435 }
436
437 diagram, _, err := d2lib.Compile(ctx, tc.script, &d2lib.CompileOptions{
438 Ruler: ruler,
439 LayoutResolver: layoutResolver,
440 FontFamily: go2.Pointer(d2fonts.HandDrawn),
441 }, renderOpts)
442 if !tassert.Nil(t, err) {
443 return
444 }
445
446 dataPath := filepath.Join("testdata", strings.TrimPrefix(t.Name(), "TestDarkTheme/"))
447 pathGotSVG := filepath.Join(dataPath, "dark_theme.got.svg")
448
449 svgBytes, err := d2svg.Render(diagram, renderOpts)
450 assert.Success(t, err)
451 err = os.MkdirAll(dataPath, 0755)
452 assert.Success(t, err)
453 err = ioutil.WriteFile(pathGotSVG, svgBytes, 0600)
454 assert.Success(t, err)
455 defer os.Remove(pathGotSVG)
456
457 var xmlParsed interface{}
458 err = xml.Unmarshal(svgBytes, &xmlParsed)
459 assert.Success(t, err)
460
461 err = diff.Testdata(filepath.Join(dataPath, "dark_theme"), ".svg", svgBytes)
462 assert.Success(t, err)
463 }
464
View as plain text