@pi-archimedes/ui 2.10.3 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/package.json +8 -8
- package/src/bash/index.ts +0 -2
- package/src/bash/renderer.test.ts +50 -5
- package/src/bash/renderer.ts +28 -30
- package/src/codemode/index.ts +0 -9
- package/src/codemode/renderer.test.ts +32 -31
- package/src/codemode/renderer.ts +29 -61
- package/src/config.test.ts +62 -1
- package/src/config.ts +45 -1
- package/src/editor/index.test.ts +118 -2
- package/src/editor/index.ts +34 -3
- package/src/editor/spin-quips.ts +1 -1
- package/src/index.screen-mode.test.ts +411 -0
- package/src/index.test.ts +506 -101
- package/src/index.ts +252 -104
- package/src/settings.test.ts +37 -2
- package/src/settings.ts +16 -3
- package/src/startup/index.test.ts +239 -129
- package/src/startup/index.ts +179 -272
- package/src/startup/pi-listing.test.ts +290 -0
- package/src/startup/sections.test.ts +38 -208
- package/src/startup/sections.ts +83 -236
- package/src/startup-splash-gate.test.ts +546 -0
- package/src/thinking/patch.test.ts +81 -1
- package/src/thinking/patch.ts +22 -6
- package/src/bash/tool.test.ts +0 -26
- package/src/bash/tool.ts +0 -17
- package/src/codemode/loader.test.ts +0 -139
- package/src/codemode/loader.ts +0 -115
- package/src/codemode/tool.test.ts +0 -149
- package/src/codemode/tool.ts +0 -96
- package/src/startup/capture.ts +0 -47
- package/src/startup/version.test.ts +0 -98
- package/src/startup/version.ts +0 -26
|
@@ -1,175 +1,285 @@
|
|
|
1
|
-
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
|
|
2
2
|
|
|
3
3
|
// ── Mocks ────────────────────────────────────────────────────────────────────
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// The splash no longer reads pi's component tree, so the only pi-tui surface
|
|
6
|
+
// these tests need is the width arithmetic `sections.ts` wraps text with. The
|
|
7
|
+
// mock keeps the same SGR-only semantics core/text's `stripAnsi` falls back to.
|
|
6
8
|
vi.mock("@earendil-works/pi-tui", () => ({
|
|
7
|
-
Text: class Text {
|
|
8
|
-
render(): string[] {
|
|
9
|
-
return ["rendered"];
|
|
10
|
-
}
|
|
11
|
-
},
|
|
12
|
-
Spacer: class Spacer {},
|
|
13
|
-
Container: class Container {
|
|
14
|
-
children: unknown[] = [];
|
|
15
|
-
addChild(c: unknown) {
|
|
16
|
-
this.children.push(c);
|
|
17
|
-
return this;
|
|
18
|
-
}
|
|
19
|
-
clear() {
|
|
20
|
-
this.children = [];
|
|
21
|
-
return this;
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
9
|
truncateToWidth: (s: string, w: number) => s.slice(0, w),
|
|
25
|
-
visibleWidth: (s: string) => s.length,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// version — no network in tests
|
|
29
|
-
vi.mock("./version.js", () => ({
|
|
30
|
-
fetchLatestVersion: vi.fn().mockResolvedValue(undefined),
|
|
31
|
-
compareVersions: () => 0,
|
|
10
|
+
visibleWidth: (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "").length,
|
|
11
|
+
stripTerminalSequences: (s: string) => s.replace(/\x1b\[[0-9;]*m/g, ""),
|
|
32
12
|
}));
|
|
33
13
|
|
|
34
14
|
// ── Imports ──────────────────────────────────────────────────────────────────
|
|
35
15
|
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
16
|
+
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { stripAnsi } from "@pi-archimedes/core/text";
|
|
18
|
+
import {
|
|
19
|
+
patchStartupListing,
|
|
20
|
+
revealStartupListing,
|
|
21
|
+
stopStartupListing,
|
|
22
|
+
renderHeader,
|
|
23
|
+
modelScopeSection,
|
|
24
|
+
type ListingRef,
|
|
25
|
+
} from "./index.js";
|
|
26
|
+
import { LOGO, LOGO_SETTLE_FRAME } from "./logo.js";
|
|
38
27
|
|
|
39
28
|
// ── Test helpers ─────────────────────────────────────────────────────────────
|
|
40
29
|
|
|
41
|
-
function makeTui(): { tui: any;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const resources = new Container();
|
|
45
|
-
const chat = new Container();
|
|
46
|
-
const doc = new Container();
|
|
47
|
-
doc.children.push(header, resources, chat);
|
|
48
|
-
const tui = {
|
|
49
|
-
terminal: { rows: 24 },
|
|
50
|
-
children: [doc],
|
|
51
|
-
requestRender: vi.fn(),
|
|
52
|
-
};
|
|
53
|
-
return { tui, resources, header };
|
|
30
|
+
function makeTui(): { tui: any; requestRender: ReturnType<typeof vi.fn> } {
|
|
31
|
+
const requestRender = vi.fn();
|
|
32
|
+
return { tui: { terminal: { rows: 24 }, requestRender }, requestRender };
|
|
54
33
|
}
|
|
55
34
|
|
|
56
35
|
function makeRef(): ListingRef {
|
|
57
36
|
return {
|
|
58
37
|
sections: [],
|
|
59
38
|
frame: 0,
|
|
60
|
-
revealed: true, // skip the debounce
|
|
39
|
+
revealed: true, // skip the reveal debounce path
|
|
61
40
|
revealedAt: 0,
|
|
62
41
|
scaffoldAt: 0,
|
|
63
42
|
settled: false,
|
|
64
43
|
};
|
|
65
44
|
}
|
|
66
45
|
|
|
67
|
-
/** Fake ExpandableText: render() returns whatever the current state yields. */
|
|
68
|
-
function makeSectionComponent(renderLines: string[], opts: { setExpanded?: ReturnType<typeof vi.fn>; getExpandedText?: () => string } = {}): any {
|
|
69
|
-
const comp: any = new (Text as any)();
|
|
70
|
-
comp.render = () => renderLines;
|
|
71
|
-
if (opts.setExpanded) comp.setExpanded = opts.setExpanded;
|
|
72
|
-
if (opts.getExpandedText) comp.getExpandedText = opts.getExpandedText;
|
|
73
|
-
return comp;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
46
|
const THEME = {
|
|
77
47
|
fg: (_token: string, t: string) => t,
|
|
78
48
|
getFgAnsi: (_token: string) => "",
|
|
79
49
|
} as any;
|
|
80
50
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (lastResources?.[DEBOUNCE_TIMER]) clearTimeout(lastResources[DEBOUNCE_TIMER]);
|
|
88
|
-
lastResources = undefined;
|
|
89
|
-
};
|
|
90
|
-
afterEach(after);
|
|
91
|
-
|
|
92
|
-
// ── patchStartupListing: section text extraction ─────────────────────────────
|
|
93
|
-
|
|
94
|
-
describe("patchStartupListing section parsing", () => {
|
|
95
|
-
it("forces expansion via setExpanded when the getExpandedText method is absent (pi ≥0.99)", () => {
|
|
96
|
-
const { tui, resources } = makeTui();
|
|
97
|
-
lastResources = resources;
|
|
98
|
-
const ref = makeRef();
|
|
99
|
-
patchStartupListing(tui, THEME, ref);
|
|
100
|
-
|
|
101
|
-
// pi ≥0.99 ExpandableText: text builders live in the constructor,
|
|
102
|
-
// the component starts collapsed. render() after setExpanded(true)
|
|
103
|
-
// returns the full per-item body.
|
|
104
|
-
const setExpanded = vi.fn();
|
|
105
|
-
const comp = makeSectionComponent(
|
|
106
|
-
["[Skills]", " /home/u/skills/alpha/SKILL.md", " /home/u/skills/beta/SKILL.md"],
|
|
107
|
-
{ setExpanded },
|
|
108
|
-
);
|
|
109
|
-
resources.addChild(comp);
|
|
110
|
-
|
|
111
|
-
expect(setExpanded).toHaveBeenCalledWith(true);
|
|
112
|
-
expect(ref.sections).toEqual([{ name: "Skills", items: ["alpha", "beta"] }]);
|
|
113
|
-
// Section consumed by the splash — original component not added
|
|
114
|
-
expect((resources as any).children.length).toBe(0);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("prefers the getExpandedText method when present (older pi)", () => {
|
|
118
|
-
const { tui, resources } = makeTui();
|
|
119
|
-
lastResources = resources;
|
|
120
|
-
const ref = makeRef();
|
|
121
|
-
patchStartupListing(tui, THEME, ref);
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
// The splash owns two module-level timers; never leak one into a test.
|
|
53
|
+
stopStartupListing();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ── renderHeader: the section set it draws ───────────────────────────────────
|
|
122
57
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
getExpandedText: () => "[Skills]\n /home/u/skills/alpha/SKILL.md",
|
|
129
|
-
},
|
|
130
|
-
);
|
|
131
|
-
resources.addChild(comp);
|
|
58
|
+
describe("renderHeader section set", () => {
|
|
59
|
+
// The rendered set is whatever the caller revealed into the ref — pi decides
|
|
60
|
+
// which of its own sections exist, so nothing here may assert a fixed list.
|
|
61
|
+
const linesFor = (sections: ListingRef["sections"]): string[] =>
|
|
62
|
+
renderHeader(THEME, { ...makeRef(), sections }, 200);
|
|
132
63
|
|
|
133
|
-
|
|
134
|
-
|
|
64
|
+
it("renders revealed sections in the order they were given", () => {
|
|
65
|
+
const lines = linesFor([
|
|
66
|
+
{ name: "Context", items: ["AGENTS.md"] },
|
|
67
|
+
{ name: "Skills", items: ["tdd"] },
|
|
68
|
+
{ name: "Prompts", items: ["/commit"] },
|
|
69
|
+
{ name: "Extensions", items: ["archimedes"] },
|
|
70
|
+
]);
|
|
71
|
+
const at = (name: string) => lines.findIndex((l) => l.includes(`[${name}]`));
|
|
72
|
+
expect(at("Context")).toBeGreaterThanOrEqual(0);
|
|
73
|
+
expect(at("Context")).toBeLessThan(at("Skills"));
|
|
74
|
+
expect(at("Skills")).toBeLessThan(at("Prompts"));
|
|
75
|
+
expect(at("Prompts")).toBeLessThan(at("Extensions"));
|
|
135
76
|
});
|
|
136
77
|
|
|
137
|
-
it("
|
|
138
|
-
const {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
78
|
+
it("renders a section name the suite has never seen before", () => {
|
|
79
|
+
const lines = linesFor([{ name: "Widgets", items: ["gauge"] }]);
|
|
80
|
+
expect(lines.some((l) => l.includes("[Widgets]"))).toBe(true);
|
|
81
|
+
expect(lines.some((l) => l.includes("gauge"))).toBe(true);
|
|
82
|
+
});
|
|
142
83
|
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
84
|
+
it("renders nothing but the version block when no section was revealed", () => {
|
|
85
|
+
const lines = linesFor([]);
|
|
86
|
+
for (const phantom of ["Models", "Context", "Prompts", "Skills", "Extensions", "Themes"]) {
|
|
87
|
+
expect(lines.some((l) => l.includes(`[${phantom}]`))).toBe(false);
|
|
88
|
+
}
|
|
89
|
+
expect(lines.some((l) => l.includes("[Version]"))).toBe(true);
|
|
90
|
+
});
|
|
146
91
|
|
|
147
|
-
|
|
148
|
-
|
|
92
|
+
it("skips a revealed section whose item list is empty", () => {
|
|
93
|
+
const lines = linesFor([
|
|
94
|
+
{ name: "Skills", items: [] },
|
|
95
|
+
{ name: "Context", items: ["AGENTS.md"] },
|
|
96
|
+
]);
|
|
97
|
+
expect(lines.some((l) => l.includes("[Skills]"))).toBe(false);
|
|
98
|
+
expect(lines.some((l) => l.includes("[Context]"))).toBe(true);
|
|
149
99
|
});
|
|
100
|
+
});
|
|
150
101
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
102
|
+
// ── The version block: pi's own version, no update check ─────────────────────
|
|
103
|
+
//
|
|
104
|
+
// The second row used to be `Latest: v…`, filled in by a
|
|
105
|
+
// `registry.npmjs.org` fetch the splash kicked off on every start. pi has its
|
|
106
|
+
// own update check (`--update`, `/changelog`, `PI_SKIP_VERSION_CHECK`), so ours
|
|
107
|
+
// is gone: the block states the running version and nothing else. Nothing here
|
|
108
|
+
// may reach the network — a startup that blocks on a socket is a startup that
|
|
109
|
+
// blocks.
|
|
110
|
+
|
|
111
|
+
describe("renderHeader version block", () => {
|
|
112
|
+
it("states the running pi version", () => {
|
|
113
|
+
const lines = renderHeader(THEME, makeRef(), 200);
|
|
114
|
+
expect(lines.some((l) => l.includes("[Version]"))).toBe(true);
|
|
115
|
+
expect(lines.some((l) => l.includes(`v${VERSION}`))).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("has no Latest row and asks no update question", () => {
|
|
119
|
+
const lines = renderHeader(THEME, makeRef(), 200);
|
|
120
|
+
expect(lines.join("\n")).not.toContain("Latest");
|
|
121
|
+
expect(lines.join("\n")).not.toContain("Local:");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── The model scope, from ctx.scopedModels ───────────────────────────────────
|
|
126
|
+
//
|
|
127
|
+
// This section used to be scraped: a `console.log` patch watched for pi's
|
|
128
|
+
// `Model scope:` line, and pi's own resource components were intercepted out of
|
|
129
|
+
// its container. Both predate `ctx.scopedModels` — the same set the
|
|
130
|
+
// `/scoped-models` command lists — which is handed to us on the context.
|
|
131
|
+
|
|
132
|
+
describe("modelScopeSection", () => {
|
|
133
|
+
it("lists the scoped models the way /scoped-models does", () => {
|
|
134
|
+
const section = modelScopeSection([
|
|
135
|
+
{ model: { id: "claude-sonnet-4-5" } },
|
|
136
|
+
{ model: { id: "gpt-5" }, thinkingLevel: "high" },
|
|
137
|
+
] as never);
|
|
138
|
+
|
|
139
|
+
expect(section).toEqual({
|
|
140
|
+
name: "Models",
|
|
141
|
+
items: ["claude-sonnet-4-5", "gpt-5:high"],
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("is absent when the session has no model scope", () => {
|
|
146
|
+
expect(modelScopeSection([])).toBeUndefined();
|
|
147
|
+
// A host without the property (older pi) is the same answer as no scope.
|
|
148
|
+
expect(modelScopeSection(undefined)).toBeUndefined();
|
|
149
|
+
});
|
|
156
150
|
|
|
157
|
-
|
|
158
|
-
|
|
151
|
+
it("ignores an entry with no model behind it", () => {
|
|
152
|
+
expect(modelScopeSection([{ thinkingLevel: "low" }] as never)).toBeUndefined();
|
|
153
|
+
expect(
|
|
154
|
+
modelScopeSection([{ model: { id: "gpt-5" } }, { thinkingLevel: "low" }] as never),
|
|
155
|
+
).toEqual({ name: "Models", items: ["gpt-5"] });
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ── The animation timer ──────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
describe("patchStartupListing animation timer", () => {
|
|
162
|
+
afterEach(() => vi.useRealTimers());
|
|
159
163
|
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
it("stops redrawing once the logo settles when no sections arrive (quietStartup)", () => {
|
|
165
|
+
vi.useFakeTimers();
|
|
166
|
+
const { tui, requestRender } = makeTui();
|
|
167
|
+
patchStartupListing(tui, { ...makeRef(), revealed: false });
|
|
168
|
+
|
|
169
|
+
vi.advanceTimersByTime(3_000);
|
|
170
|
+
const renderCountAfterSettle = requestRender.mock.calls.length;
|
|
171
|
+
vi.advanceTimersByTime(3_000);
|
|
172
|
+
expect(requestRender.mock.calls.length).toBe(renderCountAfterSettle);
|
|
162
173
|
});
|
|
163
174
|
|
|
164
|
-
it("
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
it("restarts the timer when sections are revealed after the logo settled", () => {
|
|
176
|
+
vi.useFakeTimers();
|
|
177
|
+
const { tui, requestRender } = makeTui();
|
|
178
|
+
const ref: ListingRef = { ...makeRef(), revealed: false };
|
|
179
|
+
// Real renders settle the listing, which is what stops the restarted timer
|
|
180
|
+
requestRender.mockImplementation(() => renderHeader(THEME, ref, 200));
|
|
181
|
+
patchStartupListing(tui, ref);
|
|
182
|
+
vi.advanceTimersByTime(3_000);
|
|
183
|
+
|
|
184
|
+
revealStartupListing(tui, ref, [{ name: "Models", items: ["claude-sonnet-4-5"] }]);
|
|
185
|
+
vi.advanceTimersByTime(500);
|
|
186
|
+
expect(ref.revealed).toBe(true);
|
|
187
|
+
const renderCountAfterReveal = requestRender.mock.calls.length;
|
|
188
|
+
vi.advanceTimersByTime(200);
|
|
189
|
+
expect(requestRender.mock.calls.length).toBeGreaterThan(renderCountAfterReveal);
|
|
190
|
+
|
|
191
|
+
vi.advanceTimersByTime(2_000); // past the text fade-in
|
|
192
|
+
const renderCountAfterFade = requestRender.mock.calls.length;
|
|
193
|
+
vi.advanceTimersByTime(2_000);
|
|
194
|
+
expect(requestRender.mock.calls.length).toBe(renderCountAfterFade);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("restarts from scratch when the header is rebuilt (new session, /reload)", () => {
|
|
198
|
+
vi.useFakeTimers();
|
|
199
|
+
const { tui } = makeTui();
|
|
200
|
+
const ref: ListingRef = { ...makeRef(), revealed: false };
|
|
201
|
+
patchStartupListing(tui, ref);
|
|
202
|
+
vi.advanceTimersByTime(100);
|
|
203
|
+
const frameMidAnimation = ref.frame;
|
|
204
|
+
expect(frameMidAnimation).toBeGreaterThan(0);
|
|
205
|
+
|
|
206
|
+
// The header factory is re-invoked with a fresh renderer and ref — what
|
|
207
|
+
// a new session, `/reload` or any other `setHeader()` replacement does.
|
|
208
|
+
// A `tuiMode` switch is not one of those: pi re-mounts the header it
|
|
209
|
+
// already has, which is why nothing here may hold on to a renderer.
|
|
210
|
+
const firstRenders = tui.requestRender.mock.calls.length;
|
|
211
|
+
expect(firstRenders).toBe(frameMidAnimation);
|
|
212
|
+
|
|
213
|
+
const second = makeTui();
|
|
214
|
+
const ref2: ListingRef = { ...makeRef(), revealed: false };
|
|
215
|
+
patchStartupListing(second.tui, ref2);
|
|
216
|
+
vi.advanceTimersByTime(100);
|
|
217
|
+
|
|
218
|
+
expect(ref2.frame).toBeGreaterThan(0);
|
|
219
|
+
// The first splash stopped with its own renderer: only the second one is
|
|
220
|
+
// requesting frames now.
|
|
221
|
+
expect(tui.requestRender.mock.calls.length).toBe(firstRenders);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("stopStartupListing stops both the frame timer and a pending reveal", () => {
|
|
225
|
+
vi.useFakeTimers();
|
|
226
|
+
const { tui, requestRender } = makeTui();
|
|
227
|
+
const ref: ListingRef = { ...makeRef(), revealed: false };
|
|
228
|
+
patchStartupListing(tui, ref);
|
|
229
|
+
revealStartupListing(tui, ref, [{ name: "Models", items: ["m"] }]);
|
|
230
|
+
|
|
231
|
+
stopStartupListing();
|
|
232
|
+
const renders = requestRender.mock.calls.length;
|
|
233
|
+
vi.advanceTimersByTime(3_000);
|
|
234
|
+
|
|
235
|
+
expect(requestRender.mock.calls.length).toBe(renders);
|
|
236
|
+
expect(ref.revealed).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ── Nothing else happens on the splash path ──────────────────────────────────
|
|
241
|
+
|
|
242
|
+
describe("startup splash side effects", () => {
|
|
243
|
+
beforeEach(() => {
|
|
244
|
+
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, json: async () => ({}) }) as never));
|
|
245
|
+
// Nothing may print: on fullscreen a console write lands inside the frame,
|
|
246
|
+
// and the whole reason the scrape died is that writing to stdout was the
|
|
247
|
+
// only way to learn what pi had loaded.
|
|
248
|
+
vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
afterEach(() => {
|
|
252
|
+
vi.unstubAllGlobals();
|
|
253
|
+
vi.restoreAllMocks();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("makes no network request and leaves the console alone", async () => {
|
|
257
|
+
const { tui } = makeTui();
|
|
167
258
|
const ref = makeRef();
|
|
168
|
-
patchStartupListing(tui,
|
|
259
|
+
patchStartupListing(tui, ref);
|
|
260
|
+
revealStartupListing(tui, ref, [{ name: "Models", items: ["m"] }]);
|
|
261
|
+
renderHeader(THEME, ref, 200);
|
|
262
|
+
await Promise.resolve();
|
|
263
|
+
|
|
264
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
265
|
+
// Patched by anyone? `console.log.name` reads "log" only for the original.
|
|
266
|
+
expect(console.log.name).toBe("log");
|
|
267
|
+
expect(process.stdout.write).not.toHaveBeenCalled();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ── The header renders finished content with no reveal pending ───────────────
|
|
272
|
+
|
|
273
|
+
describe("renderHeader at rest", () => {
|
|
274
|
+
it("renders the logo at rest with no listing and no stray blank rows", () => {
|
|
275
|
+
const ref: ListingRef = { ...makeRef(), frame: LOGO_SETTLE_FRAME, revealed: false };
|
|
169
276
|
|
|
170
|
-
|
|
171
|
-
resources.addChild(makeSectionComponent(["[Skills]", " beta, gamma"]));
|
|
277
|
+
const lines = renderHeader(THEME, ref, 200);
|
|
172
278
|
|
|
173
|
-
|
|
279
|
+
// Logo rows plus their separator — and, unlike frame 0, actually drawn.
|
|
280
|
+
// (Stripped: a truecolor logo colours each block character separately.)
|
|
281
|
+
expect(lines).toHaveLength(LOGO.length + 1);
|
|
282
|
+
expect(stripAnsi(lines.join("\n"))).toContain("████");
|
|
283
|
+
expect(lines.filter((l) => l.includes("[") && l.includes("]"))).toEqual([]);
|
|
174
284
|
});
|
|
175
285
|
});
|