github.com/liamawhite/cli-with-i18n@v6.32.1-0.20171122084555-dede0a5c3448+incompatible/util/ui/ui_test.go (about) 1 package ui_test 2 3 import ( 4 "errors" 5 "strings" 6 "time" 7 8 "github.com/liamawhite/cli-with-i18n/command/translatableerror/translatableerrorfakes" 9 "github.com/liamawhite/cli-with-i18n/util/configv3" 10 . "github.com/liamawhite/cli-with-i18n/util/ui" 11 "github.com/liamawhite/cli-with-i18n/util/ui/uifakes" 12 . "github.com/onsi/ginkgo" 13 . "github.com/onsi/gomega" 14 . "github.com/onsi/gomega/gbytes" 15 ) 16 17 var _ = Describe("UI", func() { 18 var ( 19 ui *UI 20 fakeConfig *uifakes.FakeConfig 21 out *Buffer 22 ) 23 24 BeforeEach(func() { 25 fakeConfig = new(uifakes.FakeConfig) 26 fakeConfig.ColorEnabledReturns(configv3.ColorEnabled) 27 28 var err error 29 ui, err = NewUI(fakeConfig) 30 Expect(err).NotTo(HaveOccurred()) 31 32 out = NewBuffer() 33 ui.Out = out 34 ui.Err = NewBuffer() 35 }) 36 37 It("sets the TimezoneLocation to the local timezone", func() { 38 location := time.Now().Location() 39 Expect(ui.TimezoneLocation).To(Equal(location)) 40 }) 41 42 Describe("DisplayBoolPrompt", func() { 43 var inBuffer *Buffer 44 45 BeforeEach(func() { 46 inBuffer = NewBuffer() 47 ui.In = inBuffer 48 }) 49 50 It("displays the passed in string", func() { 51 _, _ = ui.DisplayBoolPrompt(false, "some-prompt", nil) 52 Expect(ui.Out).To(Say("some-prompt \\[yN\\]:")) 53 }) 54 55 Context("when the user chooses yes", func() { 56 BeforeEach(func() { 57 _, err := inBuffer.Write([]byte("y\n")) 58 Expect(err).ToNot(HaveOccurred()) 59 }) 60 61 It("returns true", func() { 62 response, err := ui.DisplayBoolPrompt(false, "some-prompt", nil) 63 Expect(err).ToNot(HaveOccurred()) 64 Expect(response).To(BeTrue()) 65 }) 66 }) 67 68 Context("when the user chooses no", func() { 69 BeforeEach(func() { 70 _, err := inBuffer.Write([]byte("n\n")) 71 Expect(err).ToNot(HaveOccurred()) 72 }) 73 74 It("returns false", func() { 75 response, err := ui.DisplayBoolPrompt(false, "some-prompt", nil) 76 Expect(err).ToNot(HaveOccurred()) 77 Expect(response).To(BeFalse()) 78 }) 79 }) 80 81 Context("when the user chooses the default", func() { 82 BeforeEach(func() { 83 _, err := inBuffer.Write([]byte("\n")) 84 Expect(err).ToNot(HaveOccurred()) 85 }) 86 87 Context("when the default is true", func() { 88 It("returns true", func() { 89 response, err := ui.DisplayBoolPrompt(true, "some-prompt", nil) 90 Expect(err).ToNot(HaveOccurred()) 91 Expect(response).To(BeTrue()) 92 }) 93 }) 94 95 Context("when the default is false", func() { 96 It("returns false", func() { 97 response, err := ui.DisplayBoolPrompt(false, "some-prompt", nil) 98 Expect(err).ToNot(HaveOccurred()) 99 Expect(response).To(BeFalse()) 100 }) 101 }) 102 }) 103 104 Context("when the interact library returns an error", func() { 105 It("returns the error", func() { 106 _, err := inBuffer.Write([]byte("invalid\n")) 107 Expect(err).ToNot(HaveOccurred()) 108 _, err = ui.DisplayBoolPrompt(false, "some-prompt", nil) 109 Expect(err).To(HaveOccurred()) 110 }) 111 }) 112 }) 113 114 Describe("DisplayError", func() { 115 Context("when passed a TranslatableError", func() { 116 var fakeTranslateErr *translatableerrorfakes.FakeTranslatableError 117 118 BeforeEach(func() { 119 fakeTranslateErr = new(translatableerrorfakes.FakeTranslatableError) 120 fakeTranslateErr.TranslateReturns("I am an error") 121 122 ui.DisplayError(fakeTranslateErr) 123 }) 124 125 It("displays the error to ui.Err and displays FAILED in bold red to ui.Out", func() { 126 Expect(ui.Err).To(Say("I am an error\n")) 127 Expect(ui.Out).To(Say("\x1b\\[31;1mFAILED\x1b\\[0m\n")) 128 }) 129 130 Context("when the locale is not set to english", func() { 131 It("translates the error text", func() { 132 Expect(fakeTranslateErr.TranslateCallCount()).To(Equal(1)) 133 Expect(fakeTranslateErr.TranslateArgsForCall(0)).NotTo(BeNil()) 134 }) 135 }) 136 }) 137 138 Context("when passed a generic error", func() { 139 It("displays the error text to ui.Err and displays FAILED in bold red to ui.Out", func() { 140 ui.DisplayError(errors.New("I am a BANANA!")) 141 Expect(ui.Err).To(Say("I am a BANANA!\n")) 142 Expect(ui.Out).To(Say("\x1b\\[31;1mFAILED\x1b\\[0m\n")) 143 }) 144 }) 145 }) 146 147 Describe("DisplayHeader", func() { 148 It("displays the header colorized and bolded to ui.Out", func() { 149 ui.DisplayHeader("some-header") 150 Expect(ui.Out).To(Say("\x1b\\[1msome-header\x1b\\[0m")) 151 }) 152 153 Context("when the locale is not set to english", func() { 154 BeforeEach(func() { 155 fakeConfig.LocaleReturns("fr-FR") 156 157 var err error 158 ui, err = NewUI(fakeConfig) 159 Expect(err).NotTo(HaveOccurred()) 160 161 ui.Out = NewBuffer() 162 }) 163 164 It("displays the translated header colorized and bolded to ui.Out", func() { 165 ui.DisplayHeader("FEATURE FLAGS") 166 Expect(ui.Out).To(Say("\x1b\\[1mINDICATEURS DE FONCTION\x1b\\[0m")) 167 }) 168 }) 169 }) 170 171 Describe("DisplayKeyValueTable", func() { 172 JustBeforeEach(func() { 173 ui.DisplayKeyValueTable(" ", 174 [][]string{ 175 {"wut0:", ""}, 176 {"wut1:", "hi hi"}, 177 {"wut2:", strings.Repeat("a", 9)}, 178 {"wut3:", "hi hi " + strings.Repeat("a", 9)}, 179 {"wut4:", strings.Repeat("a", 15) + " " + strings.Repeat("b", 15)}, 180 }, 181 2) 182 }) 183 184 Context("in a TTY", func() { 185 BeforeEach(func() { 186 ui.IsTTY = true 187 ui.TerminalWidth = 20 188 }) 189 190 It("displays a table with the last column wrapping according to width", func() { 191 Expect(ui.Out).To(Say(" wut0: " + "\n")) 192 Expect(ui.Out).To(Say(" wut1: " + "hi hi\n")) 193 Expect(ui.Out).To(Say(" wut2: " + strings.Repeat("a", 9) + "\n")) 194 Expect(ui.Out).To(Say(" wut3: hi hi\n")) 195 Expect(ui.Out).To(Say(" " + strings.Repeat("a", 9) + "\n")) 196 Expect(ui.Out).To(Say(" wut4: " + strings.Repeat("a", 15) + "\n")) 197 Expect(ui.Out).To(Say(" " + strings.Repeat("b", 15) + "\n")) 198 }) 199 }) 200 }) 201 202 Describe("DisplayLogMessage", func() { 203 var message *uifakes.FakeLogMessage 204 205 BeforeEach(func() { 206 var err error 207 ui.TimezoneLocation, err = time.LoadLocation("America/Los_Angeles") 208 Expect(err).NotTo(HaveOccurred()) 209 210 message = new(uifakes.FakeLogMessage) 211 message.MessageReturns("This is a log message\r\n") 212 message.TypeReturns("OUT") 213 message.TimestampReturns(time.Unix(1468969692, 0)) // "2016-07-19T16:08:12-07:00" 214 message.SourceTypeReturns("APP/PROC/WEB") 215 message.SourceInstanceReturns("12") 216 }) 217 218 Context("with header", func() { 219 Context("single line log message", func() { 220 It("prints out a single line to STDOUT", func() { 221 ui.DisplayLogMessage(message, true) 222 Expect(ui.Out).To(Say("2016-07-19T16:08:12.00-0700 \\[APP/PROC/WEB/12\\] OUT This is a log message\n")) 223 }) 224 }) 225 226 Context("multi-line log message", func() { 227 BeforeEach(func() { 228 var err error 229 ui.TimezoneLocation, err = time.LoadLocation("America/Los_Angeles") 230 Expect(err).NotTo(HaveOccurred()) 231 232 message.MessageReturns("This is a log message\nThis is also a log message") 233 }) 234 235 It("prints out mutliple lines to STDOUT", func() { 236 ui.DisplayLogMessage(message, true) 237 Expect(ui.Out).To(Say("2016-07-19T16:08:12.00-0700 \\[APP/PROC/WEB/12\\] OUT This is a log message\n")) 238 Expect(ui.Out).To(Say("2016-07-19T16:08:12.00-0700 \\[APP/PROC/WEB/12\\] OUT This is also a log message\n")) 239 }) 240 }) 241 }) 242 243 Context("without header", func() { 244 Context("single line log message", func() { 245 It("prints out a single line to STDOUT", func() { 246 ui.DisplayLogMessage(message, false) 247 Expect(ui.Out).To(Say("This is a log message\n")) 248 }) 249 }) 250 251 Context("multi-line log message", func() { 252 BeforeEach(func() { 253 var err error 254 ui.TimezoneLocation, err = time.LoadLocation("America/Los_Angeles") 255 Expect(err).NotTo(HaveOccurred()) 256 257 message.MessageReturns("This is a log message\nThis is also a log message") 258 }) 259 260 It("prints out mutliple lines to STDOUT", func() { 261 ui.DisplayLogMessage(message, false) 262 Expect(ui.Out).To(Say("This is a log message\n")) 263 Expect(ui.Out).To(Say("This is also a log message\n")) 264 }) 265 }) 266 }) 267 268 Context("error log lines", func() { 269 BeforeEach(func() { 270 message.TypeReturns("ERR") 271 }) 272 It("colors the line red", func() { 273 ui.DisplayLogMessage(message, false) 274 Expect(ui.Out).To(Say("\x1b\\[31mThis is a log message\x1b\\[0m\n")) 275 }) 276 }) 277 }) 278 279 Describe("DisplayNewline", func() { 280 It("displays a new line", func() { 281 ui.DisplayNewline() 282 Expect(ui.Out).To(Say("\n")) 283 }) 284 }) 285 286 Describe("DisplayOK", func() { 287 It("displays 'OK' in green and bold", func() { 288 ui.DisplayOK() 289 Expect(ui.Out).To(Say("\x1b\\[32;1mOK\x1b\\[0m")) 290 }) 291 }) 292 293 Describe("DisplayTableWithHeader", func() { 294 It("makes the first row bold", func() { 295 ui.DisplayTableWithHeader(" ", 296 [][]string{ 297 {"", "header1", "header2", "header3"}, 298 {"#0", "data1", "data2", "data3"}, 299 }, 300 2) 301 Expect(ui.Out).To(Say(" \x1b\\[1mheader1\x1b\\[0m")) // Makes sure empty values are not bolded 302 Expect(ui.Out).To(Say("\x1b\\[1mheader2\x1b\\[0m")) 303 Expect(ui.Out).To(Say("\x1b\\[1mheader3\x1b\\[0m")) 304 Expect(ui.Out).To(Say("#0 data1 data2 data3")) 305 }) 306 }) 307 308 // Covers the happy paths, additional cases are tested in TranslateText 309 Describe("DisplayText", func() { 310 It("displays the template with map values substituted in to ui.Out with a newline", func() { 311 ui.DisplayText( 312 "template with {{.SomeMapValue}}", 313 map[string]interface{}{ 314 "SomeMapValue": "map-value", 315 }) 316 Expect(ui.Out).To(Say("template with map-value\n")) 317 }) 318 319 Context("when the locale is not set to english", func() { 320 BeforeEach(func() { 321 fakeConfig.LocaleReturns("fr-FR") 322 323 var err error 324 ui, err = NewUI(fakeConfig) 325 Expect(err).NotTo(HaveOccurred()) 326 327 ui.Out = NewBuffer() 328 }) 329 330 It("displays the translated template with map values substituted in to ui.Out", func() { 331 ui.DisplayText( 332 "\nTIP: Use '{{.Command}}' to target new org", 333 map[string]interface{}{ 334 "Command": "foo", 335 }) 336 Expect(ui.Out).To(Say("\nASTUCE : utilisez 'foo' pour cibler une nouvelle organisation")) 337 }) 338 }) 339 }) 340 341 Describe("DisplayTextWithFlavor", func() { 342 It("displays the template to ui.Out", func() { 343 ui.DisplayTextWithFlavor("some-template") 344 Expect(ui.Out).To(Say("some-template")) 345 }) 346 347 Context("when an optional map is passed in", func() { 348 It("displays the template with map values colorized, bolded, and substituted in to ui.Out", func() { 349 ui.DisplayTextWithFlavor( 350 "template with {{.SomeMapValue}}", 351 map[string]interface{}{ 352 "SomeMapValue": "map-value", 353 }) 354 Expect(ui.Out).To(Say("template with \x1b\\[36;1mmap-value\x1b\\[0m")) 355 }) 356 }) 357 358 Context("when multiple optional maps are passed in", func() { 359 It("displays the template with only the first map values colorized, bolded, and substituted in to ui.Out", func() { 360 ui.DisplayTextWithFlavor( 361 "template with {{.SomeMapValue}} and {{.SomeOtherMapValue}}", 362 map[string]interface{}{ 363 "SomeMapValue": "map-value", 364 }, 365 map[string]interface{}{ 366 "SomeOtherMapValue": "other-map-value", 367 }) 368 Expect(ui.Out).To(Say("template with \x1b\\[36;1mmap-value\x1b\\[0m and <no value>")) 369 }) 370 }) 371 372 Context("when the locale is not set to english", func() { 373 BeforeEach(func() { 374 fakeConfig.LocaleReturns("fr-FR") 375 376 var err error 377 ui, err = NewUI(fakeConfig) 378 Expect(err).NotTo(HaveOccurred()) 379 380 ui.Out = NewBuffer() 381 }) 382 383 It("displays the translated template with map values colorized, bolded and substituted in to ui.Out", func() { 384 ui.DisplayTextWithFlavor( 385 "App {{.AppName}} does not exist.", 386 map[string]interface{}{ 387 "AppName": "some-app-name", 388 }) 389 Expect(ui.Out).To(Say("L'application \x1b\\[36;1msome-app-name\x1b\\[0m n'existe pas.\n")) 390 }) 391 }) 392 }) 393 394 Describe("DisplayTextWithBold", func() { 395 It("displays the template to ui.Out", func() { 396 ui.DisplayTextWithBold("some-template") 397 Expect(ui.Out).To(Say("some-template")) 398 }) 399 400 Context("when an optional map is passed in", func() { 401 It("displays the template with map values bolded and substituted in to ui.Out", func() { 402 ui.DisplayTextWithBold( 403 "template with {{.SomeMapValue}}", 404 map[string]interface{}{ 405 "SomeMapValue": "map-value", 406 }) 407 Expect(ui.Out).To(Say("template with \x1b\\[1mmap-value\x1b\\[0m")) 408 }) 409 }) 410 411 Context("when multiple optional maps are passed in", func() { 412 It("displays the template with only the first map values bolded and substituted in to ui.Out", func() { 413 ui.DisplayTextWithBold( 414 "template with {{.SomeMapValue}} and {{.SomeOtherMapValue}}", 415 map[string]interface{}{ 416 "SomeMapValue": "map-value", 417 }, 418 map[string]interface{}{ 419 "SomeOtherMapValue": "other-map-value", 420 }) 421 Expect(ui.Out).To(Say("template with \x1b\\[1mmap-value\x1b\\[0m and <no value>")) 422 }) 423 }) 424 425 Context("when the locale is not set to english", func() { 426 BeforeEach(func() { 427 fakeConfig.LocaleReturns("fr-FR") 428 429 var err error 430 ui, err = NewUI(fakeConfig) 431 Expect(err).NotTo(HaveOccurred()) 432 433 ui.Out = NewBuffer() 434 }) 435 436 It("displays the translated template with map values bolded and substituted in to ui.Out", func() { 437 ui.DisplayTextWithBold( 438 "App {{.AppName}} does not exist.", 439 map[string]interface{}{ 440 "AppName": "some-app-name", 441 }) 442 Expect(ui.Out).To(Say("L'application \x1b\\[1msome-app-name\x1b\\[0m n'existe pas.\n")) 443 }) 444 }) 445 }) 446 447 // Covers the happy paths, additional cases are tested in TranslateText 448 Describe("DisplayWarning", func() { 449 It("displays the warning to ui.Err", func() { 450 ui.DisplayWarning( 451 "template with {{.SomeMapValue}}", 452 map[string]interface{}{ 453 "SomeMapValue": "map-value", 454 }) 455 Expect(ui.Err).To(Say("template with map-value")) 456 }) 457 458 Context("when the locale is not set to english", func() { 459 BeforeEach(func() { 460 fakeConfig.LocaleReturns("fr-FR") 461 462 var err error 463 ui, err = NewUI(fakeConfig) 464 Expect(err).NotTo(HaveOccurred()) 465 466 ui.Err = NewBuffer() 467 }) 468 469 It("displays the translated warning to ui.Err", func() { 470 ui.DisplayWarning( 471 "'{{.VersionShort}}' and '{{.VersionLong}}' are also accepted.", 472 map[string]interface{}{ 473 "VersionShort": "some-value", 474 "VersionLong": "some-other-value", 475 }) 476 Expect(ui.Err).To(Say("'some-value' et 'some-other-value' sont également acceptés.\n")) 477 }) 478 }) 479 }) 480 481 // Covers the happy paths, additional cases are tested in TranslateText 482 Describe("DisplayWarnings", func() { 483 It("displays the warnings to ui.Err", func() { 484 ui.DisplayWarnings([]string{"warning-1", "warning-2"}) 485 Expect(ui.Err).To(Say("warning-1\n")) 486 Expect(ui.Err).To(Say("warning-2\n")) 487 }) 488 489 Context("when the locale is not set to english", func() { 490 BeforeEach(func() { 491 fakeConfig.LocaleReturns("fr-FR") 492 493 var err error 494 ui, err = NewUI(fakeConfig) 495 Expect(err).NotTo(HaveOccurred()) 496 497 ui.Err = NewBuffer() 498 }) 499 500 It("displays the translated warnings to ui.Err", func() { 501 ui.DisplayWarnings([]string{"Also delete any mapped routes", "FEATURE FLAGS"}) 502 Expect(ui.Err).To(Say("Supprimer aussi les routes mappées\n")) 503 Expect(ui.Err).To(Say("INDICATEURS DE FONCTION\n")) 504 }) 505 }) 506 }) 507 508 Describe("RequestLoggerFileWriter", func() { 509 It("returns a RequestLoggerFileWriter with the consistent filewriting mutex", func() { 510 logger1 := ui.RequestLoggerFileWriter(nil) 511 logger2 := ui.RequestLoggerFileWriter(nil) 512 513 c := make(chan bool) 514 err := logger1.Start() 515 Expect(err).ToNot(HaveOccurred()) 516 go func() { 517 Expect(logger2.Start()).ToNot(HaveOccurred()) 518 c <- true 519 }() 520 Consistently(c).ShouldNot(Receive()) 521 Expect(logger1.Stop()).ToNot(HaveOccurred()) 522 Eventually(c).Should(Receive()) 523 }) 524 }) 525 526 Describe("RequestLoggerTerminalDisplay", func() { 527 It("returns a RequestLoggerTerminalDisplay with the consistent display mutex", func() { 528 logger1 := ui.RequestLoggerTerminalDisplay() 529 logger2 := ui.RequestLoggerTerminalDisplay() 530 531 c := make(chan bool) 532 err := logger1.Start() 533 Expect(err).ToNot(HaveOccurred()) 534 go func() { 535 Expect(logger2.Start()).ToNot(HaveOccurred()) 536 c <- true 537 }() 538 Consistently(c).ShouldNot(Receive()) 539 Expect(logger1.Stop()).ToNot(HaveOccurred()) 540 Eventually(c).Should(Receive()) 541 }) 542 }) 543 544 Describe("TranslateText", func() { 545 It("returns the template", func() { 546 Expect(ui.TranslateText("some-template")).To(Equal("some-template")) 547 }) 548 549 Context("when an optional map is passed in", func() { 550 It("returns the template with map values substituted in", func() { 551 expected := ui.TranslateText( 552 "template {{.SomeMapValue}}", 553 map[string]interface{}{ 554 "SomeMapValue": "map-value", 555 }) 556 Expect(expected).To(Equal("template map-value")) 557 }) 558 }) 559 560 Context("when multiple optional maps are passed in", func() { 561 It("returns the template with only the first map values substituted in", func() { 562 expected := ui.TranslateText( 563 "template with {{.SomeMapValue}} and {{.SomeOtherMapValue}}", 564 map[string]interface{}{ 565 "SomeMapValue": "map-value", 566 }, 567 map[string]interface{}{ 568 "SomeOtherMapValue": "other-map-value", 569 }) 570 Expect(expected).To(Equal("template with map-value and <no value>")) 571 }) 572 }) 573 574 Context("when the locale is not set to english", func() { 575 BeforeEach(func() { 576 fakeConfig.LocaleReturns("fr-FR") 577 578 var err error 579 ui, err = NewUI(fakeConfig) 580 Expect(err).NotTo(HaveOccurred()) 581 }) 582 583 It("returns the translated template", func() { 584 expected := ui.TranslateText(" View allowable quotas with 'CF_NAME quotas'") 585 Expect(expected).To(Equal(" Affichez les quotas pouvant être alloués avec 'CF_NAME quotas'")) 586 }) 587 }) 588 }) 589 590 Describe("UserFriendlyDate", func() { 591 It("formats a time into an ISO8601 string", func() { 592 Expect(ui.UserFriendlyDate(time.Unix(0, 0))).To(MatchRegexp("\\w{3} [0-3]\\d \\w{3} [0-2]\\d:[0-5]\\d:[0-5]\\d \\w+ \\d{4}")) 593 }) 594 }) 595 })