mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-11-13 12:13:42 +01:00
The line was hidden while a drop would leave the column where it is, so a drag showed nothing until it crossed a neighbour's middle. It now shows from the first movement, on the nearer edge of the column's own slot when the drop would change nothing, and at the first boundary over the mark column. The choice of boundary moves to a tested function in the columns module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
550 lines
22 KiB
TypeScript
550 lines
22 KiB
TypeScript
import type { Flow, FlowSortKey } from "@yaakapp-internal/proxy-lib";
|
|
import { describe, expect, test } from "vite-plus/test";
|
|
import customSortFixture from "../../../crates-proxy/yaak-proxy-lib/tests/fixtures/custom_column_sort.json";
|
|
import {
|
|
addCustomColumn,
|
|
addSortKey,
|
|
allColumns,
|
|
cellFilterTerm,
|
|
type ColumnPrefs,
|
|
customPath,
|
|
customPathProblem,
|
|
customValue,
|
|
defaultColumnPrefs,
|
|
dropBoundary,
|
|
fieldReference,
|
|
fieldReferenceChanges,
|
|
gridTemplate,
|
|
gridWidth,
|
|
hideOtherColumns,
|
|
moveColumn,
|
|
moveColumnBy,
|
|
nextSort,
|
|
operationOf,
|
|
parseColumnPrefs,
|
|
removeCustomColumn,
|
|
resetOrder,
|
|
resetWidth,
|
|
resetWidths,
|
|
resizeColumn,
|
|
showOperationFor,
|
|
shownColumns,
|
|
sortFlows,
|
|
toggleColumn,
|
|
toggleSortKey,
|
|
urlParts,
|
|
} from "./columns";
|
|
|
|
function flow(number: number, extra: Partial<Flow> = {}): Flow {
|
|
return {
|
|
id: `fl_${String(number).padStart(3, "0")}`,
|
|
number,
|
|
startedAt: `2026-11-09T12:00:${String(number).padStart(2, "0")}`,
|
|
url: "https://example.com/",
|
|
method: "GET",
|
|
status: 200,
|
|
error: null,
|
|
requestSize: 0,
|
|
responseSize: 0,
|
|
elapsedMs: null,
|
|
elapsedHeadersMs: null,
|
|
contentType: null,
|
|
clientName: null,
|
|
clientBundleId: null,
|
|
clientPath: null,
|
|
clientCwd: null,
|
|
clientHttpVersion: "HTTP/1.1",
|
|
upstreamHttpVersion: null,
|
|
upstreamTlsVersion: null,
|
|
remoteAddr: null,
|
|
requestHeaders: [],
|
|
responseHeaders: [],
|
|
grpcService: null,
|
|
grpcMethod: null,
|
|
graphqlOperation: null,
|
|
tunnelled: false,
|
|
...extra,
|
|
} as Flow;
|
|
}
|
|
|
|
const ids = (prefs: ColumnPrefs) => shownColumns(prefs).map((c) => c.id);
|
|
|
|
describe("shownColumns", () => {
|
|
test("shows every visible column, Query after Path", () => {
|
|
expect(ids(defaultColumnPrefs)).toEqual([
|
|
"number",
|
|
"app",
|
|
"method",
|
|
"host",
|
|
"path",
|
|
"query",
|
|
"status",
|
|
"type",
|
|
"size",
|
|
"time",
|
|
]);
|
|
});
|
|
|
|
test("follows the picker, Path included, but keeps the last column", () => {
|
|
let prefs = toggleColumn(defaultColumnPrefs, "remote");
|
|
prefs = toggleColumn(prefs, "type");
|
|
prefs = toggleColumn(prefs, "path");
|
|
expect(ids(prefs)).toContain("remote");
|
|
expect(ids(prefs)).not.toContain("type");
|
|
expect(ids(prefs)).not.toContain("path");
|
|
const alone = hideOtherColumns(defaultColumnPrefs, "host");
|
|
expect(ids(alone)).toEqual(["host"]);
|
|
expect(toggleColumn(alone, "host")).toBe(alone);
|
|
// Where App has nothing to show, the last other column is the last one.
|
|
const withApp = hideOtherColumns(defaultColumnPrefs, "app");
|
|
const both = toggleColumn(withApp, "host");
|
|
expect(toggleColumn(both, "host", ["app"])).toBe(both);
|
|
expect(ids(both)).toEqual(["app", "host"]);
|
|
expect(shownColumns(both, ["app"]).map((c) => c.id)).toEqual(["host"]);
|
|
});
|
|
|
|
test("lays out every column at a fixed width", () => {
|
|
let prefs = resizeColumn(defaultColumnPrefs, "host", 240.4);
|
|
prefs = resizeColumn(prefs, "path", 500);
|
|
const columns = shownColumns(prefs);
|
|
const pad = (w: number) => `calc(${w}px + 0.75rem)`;
|
|
expect(gridTemplate(columns)).toBe(
|
|
["24px", ...[46, 20, 50, 240, 500, 120, 46, 50, 72, 64].map(pad)].join(" "),
|
|
);
|
|
expect(gridWidth(columns)).toBe("calc(1232px + 10 * 0.75rem)");
|
|
});
|
|
});
|
|
|
|
describe("column preferences", () => {
|
|
test("persist through storage and drop what no longer applies", () => {
|
|
let prefs = resizeColumn(defaultColumnPrefs, "host", 10);
|
|
prefs = toggleColumn(prefs, "number");
|
|
prefs = addCustomColumn(prefs, "header.x-cache");
|
|
prefs = { ...prefs, sort: [{ column: "status", descending: true }] };
|
|
const stored = JSON.parse(JSON.stringify(prefs));
|
|
expect(parseColumnPrefs(stored)).toEqual({
|
|
visible: { number: false, "field:header.x-cache": true },
|
|
widths: { host: 60 },
|
|
custom: ["field:header.x-cache"],
|
|
order: [],
|
|
sort: [{ column: "status", descending: true }],
|
|
});
|
|
expect(
|
|
parseColumnPrefs({
|
|
visible: { gone: true, size: "yes" },
|
|
widths: { time: -1, size: 90 },
|
|
custom: ["field:nope.x", 3, "field:query.page"],
|
|
sort: [
|
|
{ column: "gone", descending: true },
|
|
{ column: "host" },
|
|
{ column: "time", descending: false },
|
|
],
|
|
}),
|
|
).toEqual({
|
|
visible: {},
|
|
widths: { size: 90 },
|
|
custom: ["field:query.page"],
|
|
order: [],
|
|
sort: [{ column: "time", descending: false }],
|
|
});
|
|
expect(parseColumnPrefs(null)).toEqual(defaultColumnPrefs);
|
|
expect(resetWidths(prefs).widths).toEqual({});
|
|
});
|
|
|
|
test("a double-click resets one column's width", () => {
|
|
let prefs = resizeColumn(defaultColumnPrefs, "host", 300);
|
|
prefs = resizeColumn(prefs, "path", 400);
|
|
expect(resetWidth(prefs, "path").widths).toEqual({ host: 300 });
|
|
});
|
|
|
|
test("custom columns are added at the end and removed with their settings", () => {
|
|
let prefs = addCustomColumn(defaultColumnPrefs, "res.header.x-cache");
|
|
prefs = resizeColumn(prefs, "field:res.header.x-cache", 200);
|
|
expect(ids(prefs).at(-1)).toBe("field:res.header.x-cache");
|
|
prefs = removeCustomColumn(prefs, "field:res.header.x-cache");
|
|
expect(prefs).toEqual(defaultColumnPrefs);
|
|
});
|
|
|
|
test("a custom column's sort persists with it and goes when it does", () => {
|
|
let prefs = addCustomColumn(defaultColumnPrefs, "header.x-cache");
|
|
const sort: FlowSortKey[] = [
|
|
{ column: "field:header.x-cache", descending: true },
|
|
{ column: "status", descending: false },
|
|
];
|
|
prefs = { ...prefs, sort };
|
|
expect(parseColumnPrefs(JSON.parse(JSON.stringify(prefs))).sort).toEqual(sort);
|
|
// A key for a column that is no longer there is dropped
|
|
expect(parseColumnPrefs({ ...prefs, custom: [] }).sort).toEqual([sort[1]]);
|
|
expect(removeCustomColumn(prefs, "field:header.x-cache").sort).toEqual([sort[1]]);
|
|
});
|
|
|
|
test("Operation shows itself once, unless the person set it", () => {
|
|
const grpc = flow(1, { grpcService: "pkg.Greeter", grpcMethod: "SayHello" });
|
|
const anonymous = flow(2, { graphqlOperation: "" });
|
|
expect(showOperationFor(defaultColumnPrefs, [flow(3), anonymous])).toBe(defaultColumnPrefs);
|
|
expect(showOperationFor(defaultColumnPrefs, [grpc]).visible.operation).toBe(true);
|
|
const hidden = { ...defaultColumnPrefs, visible: { operation: false } };
|
|
expect(showOperationFor(hidden, [grpc])).toBe(hidden);
|
|
});
|
|
});
|
|
|
|
describe("column order", () => {
|
|
const defaults = ids(defaultColumnPrefs);
|
|
|
|
test("a column moves to the boundary it was dropped at", () => {
|
|
expect(ids(moveColumn(defaultColumnPrefs, "status", 2))).toEqual([
|
|
"number",
|
|
"app",
|
|
"status",
|
|
"method",
|
|
"host",
|
|
"path",
|
|
"query",
|
|
"type",
|
|
"size",
|
|
"time",
|
|
]);
|
|
// Rightwards, before the column at the boundary
|
|
expect(ids(moveColumn(defaultColumnPrefs, "method", 5)).slice(2, 6)).toEqual([
|
|
"host",
|
|
"path",
|
|
"method",
|
|
"query",
|
|
]);
|
|
// To either end
|
|
expect(ids(moveColumn(defaultColumnPrefs, "time", 0))[0]).toBe("time");
|
|
expect(ids(moveColumn(defaultColumnPrefs, "number", defaults.length)).at(-1)).toBe("number");
|
|
// The picker lists them in the same order
|
|
const moved = moveColumn(defaultColumnPrefs, "time", 0);
|
|
expect(allColumns(moved)[0]?.id).toBe("time");
|
|
});
|
|
|
|
test("a drop where the column already is changes nothing", () => {
|
|
expect(moveColumn(defaultColumnPrefs, "host", 3)).toBe(defaultColumnPrefs);
|
|
expect(moveColumn(defaultColumnPrefs, "host", 4)).toBe(defaultColumnPrefs);
|
|
expect(moveColumn(defaultColumnPrefs, "host", -1)).toBe(defaultColumnPrefs);
|
|
expect(moveColumn(defaultColumnPrefs, "host", defaults.length + 1)).toBe(defaultColumnPrefs);
|
|
// A hidden column has no place among those shown
|
|
expect(moveColumn(defaultColumnPrefs, "latency", 0)).toBe(defaultColumnPrefs);
|
|
});
|
|
|
|
test("Move Left and Move Right go one place, and not past the ends", () => {
|
|
expect(ids(moveColumnBy(defaultColumnPrefs, "host", -1)).slice(2, 5)).toEqual([
|
|
"host",
|
|
"method",
|
|
"path",
|
|
]);
|
|
expect(ids(moveColumnBy(defaultColumnPrefs, "host", 1)).slice(3, 6)).toEqual([
|
|
"path",
|
|
"host",
|
|
"query",
|
|
]);
|
|
expect(moveColumnBy(defaultColumnPrefs, "number", -1)).toBe(defaultColumnPrefs);
|
|
expect(moveColumnBy(defaultColumnPrefs, "time", 1)).toBe(defaultColumnPrefs);
|
|
// Where App has nothing to show, Number is next to Method
|
|
expect(
|
|
shownColumns(moveColumnBy(defaultColumnPrefs, "method", -1, ["app"]), ["app"]).map(
|
|
(c) => c.id,
|
|
)[0],
|
|
).toBe("method");
|
|
});
|
|
|
|
test("a hidden column keeps its place and comes back to it", () => {
|
|
let prefs = moveColumn(defaultColumnPrefs, "time", 0);
|
|
prefs = toggleColumn(prefs, "status");
|
|
prefs = moveColumn(prefs, "host", 0);
|
|
expect(ids(prefs)).not.toContain("status");
|
|
prefs = toggleColumn(prefs, "status");
|
|
expect(ids(prefs)).toEqual([
|
|
"host",
|
|
"time",
|
|
"number",
|
|
"app",
|
|
"method",
|
|
"path",
|
|
"query",
|
|
"status",
|
|
"type",
|
|
"size",
|
|
]);
|
|
});
|
|
|
|
test("a column turned on goes at the end, unless it had a place", () => {
|
|
// Request Size's default place is before Size
|
|
let prefs = toggleColumn(defaultColumnPrefs, "requestSize");
|
|
expect(ids(prefs).at(-1)).toBe("requestSize");
|
|
expect(ids(prefs).slice(0, -1)).toEqual(defaults);
|
|
// Moved, hidden and shown again, it goes back where it was moved to
|
|
prefs = moveColumn(prefs, "requestSize", defaults.indexOf("size"));
|
|
prefs = toggleColumn(toggleColumn(prefs, "requestSize"), "requestSize");
|
|
expect(ids(prefs).slice(-3)).toEqual(["requestSize", "size", "time"]);
|
|
// Already last of those shown, nothing needs placing
|
|
expect(toggleColumn(defaultColumnPrefs, "remote").order).toEqual([]);
|
|
});
|
|
|
|
test("a custom column is added at the end and leaves the order when removed", () => {
|
|
const moved = moveColumn(defaultColumnPrefs, "size", 0);
|
|
let prefs = addCustomColumn(moved, "header.x-cache");
|
|
expect(ids(prefs).at(-1)).toBe("field:header.x-cache");
|
|
prefs = moveColumn(prefs, "field:header.x-cache", 1);
|
|
expect(ids(prefs).slice(0, 2)).toEqual(["size", "field:header.x-cache"]);
|
|
// Hidden and added again, it keeps its place
|
|
prefs = addCustomColumn(toggleColumn(prefs, "field:header.x-cache"), "header.x-cache");
|
|
expect(ids(prefs)[1]).toBe("field:header.x-cache");
|
|
prefs = removeCustomColumn(prefs, "field:header.x-cache");
|
|
expect(prefs.order).not.toContain("field:header.x-cache");
|
|
expect(ids(prefs)).toEqual(ids(moved));
|
|
// Added again, it starts over at the end
|
|
expect(ids(addCustomColumn(prefs, "header.x-cache")).at(-1)).toBe("field:header.x-cache");
|
|
});
|
|
|
|
test("the order persists, and columns it doesn't list follow it in their default order", () => {
|
|
let prefs = addCustomColumn(defaultColumnPrefs, "query.page");
|
|
prefs = moveColumn(prefs, "field:query.page", 0);
|
|
expect(parseColumnPrefs(JSON.parse(JSON.stringify(prefs))).order).toEqual(prefs.order);
|
|
const parsed = parseColumnPrefs({
|
|
custom: ["field:query.page"],
|
|
order: ["time", "gone", 3, "time", "field:header.x-gone", "field:query.page", "host"],
|
|
});
|
|
expect(parsed.order).toEqual(["time", "field:query.page", "host"]);
|
|
expect(ids(parsed)).toEqual([
|
|
"time",
|
|
"field:query.page",
|
|
"host",
|
|
"number",
|
|
"app",
|
|
"method",
|
|
"path",
|
|
"query",
|
|
"status",
|
|
"type",
|
|
"size",
|
|
]);
|
|
expect(parseColumnPrefs({ order: "time" }).order).toEqual([]);
|
|
});
|
|
|
|
test("the drop line shows at every boundary, its own slot's edges included", () => {
|
|
// Four columns 100px wide from x 24, after the mark column; the third is dragged
|
|
const cells = [24, 124, 224, 324].map((left) => ({ left, right: left + 100 }));
|
|
const at = (x: number) => dropBoundary(x, cells, 2);
|
|
// Over the mark column or the first's left half: the first boundary
|
|
expect(at(5)).toEqual({ to: 0, line: 24 });
|
|
expect(at(60)).toEqual({ to: 0, line: 24 });
|
|
expect(at(80)).toEqual({ to: 1, line: 124 });
|
|
// The left neighbour's near half and its own left half: its left edge, staying
|
|
expect(at(190)).toEqual({ to: null, line: 224 });
|
|
expect(at(250)).toEqual({ to: null, line: 224 });
|
|
// Its own right half and the right neighbour's near half: its right edge, staying
|
|
expect(at(290)).toEqual({ to: null, line: 324 });
|
|
expect(at(350)).toEqual({ to: null, line: 324 });
|
|
// Past the right neighbour's middle, and past the end
|
|
expect(at(380)).toEqual({ to: 4, line: 424 });
|
|
expect(at(900)).toEqual({ to: 4, line: 424 });
|
|
// The last column dragged to the end stays
|
|
expect(dropBoundary(900, cells, 3)).toEqual({ to: null, line: 424 });
|
|
});
|
|
|
|
test("Reset Column Order puts every column back", () => {
|
|
let prefs = moveColumn(defaultColumnPrefs, "time", 0);
|
|
prefs = toggleColumn(prefs, "latency");
|
|
prefs = resetOrder(prefs);
|
|
expect(prefs.order).toEqual([]);
|
|
expect(ids(prefs)).toEqual([...defaults.slice(0, -1), "time", "latency"]);
|
|
});
|
|
});
|
|
|
|
describe("sorting", () => {
|
|
const one = (column: FlowSortKey["column"], descending: boolean) => [{ column, descending }];
|
|
|
|
test("a click sorts by a column alone, reverses it, then goes back to arrival order", () => {
|
|
expect(nextSort([], "size")).toEqual(one("size", true));
|
|
expect(nextSort([], "host")).toEqual(one("host", false));
|
|
expect(nextSort(one("host", false), "host")).toEqual(one("host", true));
|
|
expect(nextSort(one("host", true), "host")).toEqual([]);
|
|
expect(nextSort(one("size", true), "size")).toEqual(one("size", false));
|
|
expect(nextSort(one("size", false), "size")).toEqual([]);
|
|
// Another column, or a sort on several, starts over.
|
|
expect(nextSort(one("host", true), "size")).toEqual(one("size", true));
|
|
const two = [...one("host", false), ...one("size", true)];
|
|
expect(nextSort(two, "host")).toEqual(one("host", false));
|
|
});
|
|
|
|
test("Option-click adds a key ascending, reverses it, then takes it out", () => {
|
|
let sort = toggleSortKey(one("status", true), "host");
|
|
expect(sort).toEqual([...one("status", true), ...one("host", false)]);
|
|
sort = toggleSortKey(sort, "host");
|
|
expect(sort).toEqual([...one("status", true), ...one("host", true)]);
|
|
expect(toggleSortKey(sort, "host")).toEqual(one("status", true));
|
|
expect(addSortKey(sort, "status", false)).toEqual([
|
|
...one("status", false),
|
|
...one("host", true),
|
|
]);
|
|
expect(addSortKey([], "time", true)).toEqual(one("time", true));
|
|
});
|
|
|
|
test("a custom column sorts on click and Option-click as the others do", () => {
|
|
const cache = "field:header.x-cache";
|
|
expect(nextSort([], cache)).toEqual(one(cache, false));
|
|
expect(nextSort(one(cache, false), cache)).toEqual(one(cache, true));
|
|
expect(toggleSortKey(one("status", true), cache)).toEqual([
|
|
...one("status", true),
|
|
...one(cache, false),
|
|
]);
|
|
});
|
|
|
|
// Flows and the order each sort lists them in, shared with
|
|
// `a_custom_column_sorts_by_its_value_and_pages` in the lib's `flows.rs`, which checks the
|
|
// database lists them in the same order.
|
|
test("a custom column sorts as the database does", () => {
|
|
const fixture = customSortFixture as {
|
|
flows: Partial<Flow>[];
|
|
sorts: { sort: FlowSortKey[]; order: number[] }[];
|
|
};
|
|
const flows = fixture.flows.map((f, i) => flow(i, f));
|
|
for (const { sort, order } of fixture.sorts) {
|
|
expect(
|
|
sortFlows(flows, sort).map((f) => f.number),
|
|
JSON.stringify(sort),
|
|
).toEqual(order);
|
|
}
|
|
});
|
|
|
|
// The same flows and orders as `a_sort_on_several_columns_pages_in_its_order` in the lib's
|
|
// `flows.rs`, so the app places a flow where the database would.
|
|
const flows = [
|
|
flow(0, { url: "https://B.com/x", status: 200 }),
|
|
flow(1, { url: "https://a.com/y", status: 404 }),
|
|
flow(2, { url: "https://a.com/z", status: null }),
|
|
flow(3, { url: "https://c.com/", status: 200 }),
|
|
flow(4, { url: "https://a.com/", status: null, error: "boom" }),
|
|
flow(5, { url: "https://b.com/q", status: 200 }),
|
|
flow(6, { url: "https://A.com/", status: 404 }),
|
|
flow(7, { url: "https://c.com/", status: 200 }),
|
|
];
|
|
const numbers = (sort: FlowSortKey[]) => sortFlows(flows, sort).map((f) => f.number);
|
|
|
|
test("sorts on several columns as the database does", () => {
|
|
const status = [...one("status", true), ...one("host", false)];
|
|
expect(numbers(status)).toEqual([1, 6, 0, 5, 3, 7, 4, 2]);
|
|
const ascending = [...one("status", false), ...one("host", false)];
|
|
expect(numbers(ascending)).toEqual([2, 4, 0, 5, 3, 7, 1, 6]);
|
|
expect(numbers(one("query", true))).toEqual([0, 1, 2, 3, 4, 5, 6, 7]);
|
|
expect(sortFlows(flows, [])).toBe(flows);
|
|
});
|
|
|
|
test("sizes, times and the rest put flows without a value lowest", () => {
|
|
const a = flow(1, { responseSize: 10, elapsedMs: 5, requestSize: 3, elapsedHeadersMs: 2 });
|
|
const b = flow(2, { responseSize: 300, elapsedMs: null, requestSize: 0 });
|
|
const c = flow(3, { responseSize: 10, elapsedMs: 50, elapsedHeadersMs: 40 });
|
|
const d = flow(4, { error: "DNS lookup failed", status: null, responseSize: 99 });
|
|
const four = [a, b, c, d];
|
|
const by = (column: FlowSortKey["column"], descending: boolean) =>
|
|
sortFlows(four, one(column, descending)).map((f) => f.number);
|
|
expect(by("size", true)).toEqual([2, 1, 3, 4]);
|
|
expect(by("size", false)).toEqual([4, 1, 3, 2]);
|
|
expect(by("time", true)).toEqual([3, 1, 2, 4]);
|
|
expect(by("latency", true)).toEqual([3, 1, 2, 4]);
|
|
expect(by("requestSize", true)).toEqual([1, 2, 3, 4]);
|
|
expect(by("number", true)).toEqual([4, 3, 2, 1]);
|
|
// Failures sort below every status.
|
|
expect(by("status", true)).toEqual([1, 2, 3, 4]);
|
|
expect(by("status", false)).toEqual([4, 1, 2, 3]);
|
|
});
|
|
});
|
|
|
|
describe("values", () => {
|
|
test("splits URLs as the lib does", () => {
|
|
expect(urlParts("https://a.com:8080/v1/x?page=2&q=1#top")).toEqual({
|
|
authority: "a.com:8080",
|
|
path: "/v1/x",
|
|
query: "page=2&q=1",
|
|
});
|
|
expect(urlParts("https://a.com?x")).toEqual({ authority: "a.com", path: "", query: "x" });
|
|
expect(urlParts("bank.example.com:443")).toEqual({
|
|
authority: "bank.example.com:443",
|
|
path: "",
|
|
query: null,
|
|
});
|
|
});
|
|
|
|
test("reads a custom column's path from what was typed", () => {
|
|
expect(customPath("@header.X-Request-Id:")).toBe("header.x-request-id");
|
|
expect(customPath("query.page")).toBe("query.page");
|
|
expect(customPath(" res.header.x-cache ")).toBe("res.header.x-cache");
|
|
expect(customPath("@host")).toBeNull();
|
|
expect(customPath("header.")).toBeNull();
|
|
expect(customPath("header.a b")).toBeNull();
|
|
expect(customPath("query.filter[status]")).toBeNull();
|
|
});
|
|
|
|
test("says tersely why what was typed names no column", () => {
|
|
expect(customPathProblem("")).toBeNull();
|
|
expect(customPathProblem("@")).toBeNull();
|
|
expect(customPathProblem("@req.header.user-agent")).toBeNull();
|
|
expect(customPathProblem("@hea")).toBe("Incomplete");
|
|
expect(customPathProblem("@res.header")).toBe("Incomplete");
|
|
expect(customPathProblem("@header.")).toBe("Missing name");
|
|
expect(customPathProblem("@query.a b")).toBe("Invalid name");
|
|
expect(customPathProblem("@host")).toBe("Unknown field");
|
|
});
|
|
|
|
test("keeps the Add Column field a reference to a field", () => {
|
|
expect(fieldReference("")).toBe("");
|
|
expect(fieldReference("@")).toBe("@");
|
|
expect(fieldReference("h")).toBe("@h");
|
|
expect(fieldReference("header.x-cache")).toBe("@header.x-cache");
|
|
expect(fieldReference("@req.header.user-agent:")).toBe("@req.header.user-agent");
|
|
expect(fieldReference(" @@Header.X-Cache: ")).toBe("@Header.X-Cache");
|
|
expect(fieldReference(":")).toBe("");
|
|
expect(fieldReference("@:")).toBe("@");
|
|
expect(fieldReferenceChanges("@header.x")).toEqual([]);
|
|
expect(fieldReferenceChanges("query.page:")).toEqual([
|
|
{ from: 0, to: 0, insert: "@" },
|
|
{ from: 10, to: 11, insert: "" },
|
|
]);
|
|
});
|
|
|
|
test("a custom column shows the first value, the response's header before the request's", () => {
|
|
const f = flow(1, {
|
|
url: "https://a.com/?Page=2&page=3",
|
|
requestHeaders: [
|
|
{ name: "X-Cache", value: "req" },
|
|
{ name: "Accept", value: "a" },
|
|
{ name: "accept", value: "b" },
|
|
],
|
|
responseHeaders: [{ name: "x-cache", value: "HIT" }],
|
|
});
|
|
expect(customValue(f, "header.x-cache")).toBe("HIT");
|
|
expect(customValue(f, "req.header.x-cache")).toBe("req");
|
|
expect(customValue(f, "header.accept")).toBe("a");
|
|
expect(customValue(f, "query.page")).toBe("2");
|
|
expect(customValue(f, "query.nope")).toBeNull();
|
|
});
|
|
|
|
test("a cell's filter terms, where its column has a field to match", () => {
|
|
const f = flow(1, {
|
|
url: "https://api.stripe.com:443/v1/charges?x=1",
|
|
status: 404,
|
|
clientName: "Google Chrome",
|
|
contentType: "application/json; charset=utf-8",
|
|
responseHeaders: [{ name: "X-Cache", value: "HIT" }],
|
|
});
|
|
expect(cellFilterTerm(f, "host", false)).toBe("@host:api.stripe.com");
|
|
expect(cellFilterTerm(f, "status", true)).toBe("-@status:404");
|
|
expect(cellFilterTerm(f, "app", false)).toBe('@app:"Google Chrome"');
|
|
expect(cellFilterTerm(f, "path", false)).toBe("@path:/v1/charges");
|
|
expect(cellFilterTerm(f, "type", false)).toBe("@type:application/json");
|
|
expect(cellFilterTerm(f, "field:header.x-cache", false)).toBe("@header.x-cache:HIT");
|
|
for (const column of ["number", "size", "time", "query", "started"] as const) {
|
|
expect(cellFilterTerm(f, column, false)).toBeNull();
|
|
}
|
|
const grpc = flow(2, { grpcService: "pkg.Greeter", grpcMethod: "SayHello" });
|
|
expect(operationOf(grpc)).toBe("pkg.Greeter/SayHello");
|
|
expect(cellFilterTerm(grpc, "operation", true)).toBe(
|
|
"-(@grpc.service:pkg.Greeter @grpc.method:SayHello)",
|
|
);
|
|
const graphql = flow(3, { graphqlOperation: "GetUser" });
|
|
expect(cellFilterTerm(graphql, "operation", false)).toBe("@graphql.operation:GetUser");
|
|
});
|
|
});
|