diff --git a/src/test/matchers/toHaveFgColor.ts b/src/test/matchers/toHaveFgColor.ts new file mode 100644 index 0000000..2e9a75a --- /dev/null +++ b/src/test/matchers/toHaveFgColor.ts @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { MatcherContext, AsyncExpectationResult } from "expect"; +import convert from "color-convert"; +import chalk from "chalk"; + +import { getExpectTimeout } from "../../config/config.js"; +import { Cell, Locator } from "../../terminal/locator.js"; + +export async function toHaveFgColor( + this: MatcherContext, + locator: Locator, + expected: string | number | [number, number, number], + options?: { timeout?: number } +): AsyncExpectationResult { + const cells = await locator.resolve(options?.timeout ?? getExpectTimeout()); + const [result, errorCell] = hasFgColor( + cells ?? [], + expected, + this.isNot ?? false + ); + const pass = this.isNot ? !result : result; + const badColor = toMatchingColorMode(expected, errorCell); + + return { + pass, + message: () => { + if (!pass && !this.isNot) { + return ( + `expect(${chalk.red("received")}).toHaveFgColor(${chalk.green("expected")})` + + `\n\nExpected Color: ${chalk.green(expected.toString())}\nFound Color: ${chalk.red(badColor)} in cell "${errorCell?.termCell?.getChars()}" at ${errorCell?.x},${errorCell?.y}` + ); + } + if (pass && this.isNot) { + return ( + `expect(${chalk.red("received")}).not.toHaveFgColor(${chalk.green("expected")})` + + `\n\nExpected No Occurrences Of Color: ${chalk.green(expected.toString())}\nFound Color: ${chalk.red(badColor)} in cell "${errorCell?.termCell?.getChars()}" at ${errorCell?.x},${errorCell?.y}` + ); + } + return "passed"; + }, + }; +} + +function toMatchingColorMode( + expected: string | number | [number, number, number], + cell?: Cell +): string { + if (cell == null) return ""; + + const { termCell } = cell; + if (typeof expected == "string") { + return termCell?.isFgDefault() + ? "000000" + : termCell?.isFgPalette() + ? convert.ansi256.hex(termCell.getFgColor()) + : termCell?.getFgColor().toString(16) ?? ""; + } else if (Array.isArray(expected)) { + return termCell?.isFgDefault() + ? "[0,0,0]" + : termCell?.isFgPalette() + ? convert.ansi256.rgb(termCell.getFgColor()).toString() + : convert.hex.rgb(termCell!.getFgColor().toString(16)).toString(); + } else { + return termCell?.isFgDefault() + ? "0" + : termCell?.isFgPalette() + ? termCell.getFgColor().toString() + : convert.hex.ansi256(termCell!.getFgColor().toString(16)).toString(); + } +} + +function hasFgColor( + cells: Cell[], + color: string | number | [number, number, number], + isNot: boolean +): [boolean, Cell | undefined] { + if (Array.isArray(color)) { + const [red, green, blue] = color; + const badCells = cells.filter((cell) => { + const { termCell } = cell; + const valid = termCell?.isFgDefault() + ? red == 0 && blue == 0 && green == 0 + : termCell?.isFgPalette() + ? termCell.getFgColor() == convert.rgb.ansi256(color) + : termCell?.getFgColor().toString(16) === convert.rgb.hex(color); + return isNot ? valid : !valid; + }); + if (badCells.length > 0) return [false, badCells[0]]; + } else if (typeof color == "number") { + const badCells = cells.filter((cell) => { + const { termCell } = cell; + const valid = termCell?.isFgDefault() + ? color === -1 || color === 0 + : termCell?.isFgPalette() + ? termCell.getFgColor() === color + : termCell?.getFgColor().toString(16) === convert.ansi256.hex(color); + return isNot ? valid : !valid; + }); + if (badCells.length > 0) return [false, badCells[0]]; + } else if (typeof color == "string") { + const badCells = cells.filter((cell) => { + const { termCell } = cell; + const valid = termCell?.isFgDefault() + ? convert.hex.ansi256(color) === 0 + : termCell?.isFgPalette() + ? termCell.getFgColor() === convert.hex.ansi256(color) + : termCell?.getFgColor().toString(16) === color; + return isNot ? valid : !valid; + }); + if (badCells.length > 0) return [false, badCells[0]]; + } + return [true, undefined]; +} diff --git a/src/test/test.ts b/src/test/test.ts index b27b7f6..d888ac4 100644 --- a/src/test/test.ts +++ b/src/test/test.ts @@ -17,6 +17,7 @@ import { toMatchSnapshot } from "./matchers/toMatchSnapshot.js"; import { Terminal } from "../terminal/term.js"; import { TestConfig } from "../config/config.js"; import { toHaveBgColor } from "./matchers/toHaveBgColor.js"; +import { toHaveFgColor } from "./matchers/toHaveFgColor.js"; import { Locator } from "../terminal/locator.js"; import { toBeVisible } from "./matchers/toBeVisible.js"; @@ -258,6 +259,7 @@ jestExpect.extend({ toBeVisible, toMatchSnapshot, toHaveBgColor, + toHaveFgColor, }); interface TerminalMatchers { @@ -312,6 +314,31 @@ interface LocatorMatchers { timeout?: number; } ): Promise; + + /** + * Checks that selected text has the desired foreground color. + * + * **Usage** + * + * ```js + * await expect(terminal.getByText(">")).toHaveFgColor("#000000"); + * ``` + * + * @param value The desired cell's foreground color. Can be in the following forms + * - ANSI 256: This is a number from 0 to 255 of ANSI colors `255` + * - Hex: A string representing a 'true color' `#FFFFFF` + * - RGB: An array presenting an rgb color `[255, 255, 255]` + * @param options + */ + toHaveFgColor( + value: string | number | [number, number, number], + options?: { + /** + * Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`. + */ + timeout?: number; + } + ): Promise; } declare type BaseMatchers = Matchers & diff --git a/test/e2e.test.ts b/test/e2e.test.ts index a89318d..cb7e18b 100644 --- a/test/e2e.test.ts +++ b/test/e2e.test.ts @@ -200,11 +200,33 @@ test.describe("locators", () => { }); test.describe("color detection", () => { - test("checks background color", async ({ terminal }) => { + test("checks the default background color", async ({ terminal }) => { await expect(terminal.getByText(">")).toHaveBgColor(0); await expect(terminal.getByText(">")).toHaveBgColor([0, 0, 0]); }); + test("checks the default foreground color", async ({ terminal }) => { + await expect(terminal.getByText(">")).toHaveFgColor(0); + }); + + test.when( + os.platform() === "linux", + "checks background color", + async ({ terminal }) => { + terminal.write(String.raw`printf \033[41mHello\n\033[0m\r`); + await expect(terminal.getByText("Hello ")).toHaveBgColor(41); + } + ); + + test.when( + os.platform() === "linux", + "checks foreground color", + async ({ terminal }) => { + terminal.write(String.raw`printf \033[31mHello\n\033[0m\r`); + await expect(terminal.getByText("Hello ")).toHaveFgColor(31); + } + ); + test.fail( "checks failure on background color when it doesn't match", async ({ terminal }) => {