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