Skip to content

Commit

Permalink
feat: add toHaveFgColor matcher
Browse files Browse the repository at this point in the history
Signed-off-by: Chapman Pendery <[email protected]>
  • Loading branch information
cpendery committed Feb 9, 2024
1 parent 2e61345 commit f381903
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 1 deletion.
115 changes: 115 additions & 0 deletions src/test/matchers/toHaveFgColor.ts
Original file line number Diff line number Diff line change
@@ -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];
}
27 changes: 27 additions & 0 deletions src/test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -258,6 +259,7 @@ jestExpect.extend({
toBeVisible,
toMatchSnapshot,
toHaveBgColor,
toHaveFgColor,
});

interface TerminalMatchers {
Expand Down Expand Up @@ -312,6 +314,31 @@ interface LocatorMatchers {
timeout?: number;
}
): Promise<void>;

/**
* 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<void>;
}

declare type BaseMatchers<T> = Matchers<void, T> &
Expand Down
24 changes: 23 additions & 1 deletion test/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
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`);
await expect(terminal.getByText("Hello ")).toHaveFgColor(31);
}
);

test.fail(
"checks failure on background color when it doesn't match",
async ({ terminal }) => {
Expand Down

0 comments on commit f381903

Please sign in to comment.