@alloy-js/core 0.22.0 → 0.23.0-dev.1

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.
@@ -0,0 +1,235 @@
1
+ import { Output, SourceDirectory, SourceFile } from "@alloy-js/core";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { createNamedContext } from "../../src/context.js";
4
+ import { clearRenderStack } from "../../src/render-stack.js";
5
+ import { renderTree } from "../../src/render.js";
6
+ import "../../testing/extend-expect.js";
7
+
8
+ // Strip ANSI escape codes from a string for consistent testing across environments
9
+ function stripAnsi(str: string): string {
10
+ // eslint-disable-next-line no-control-regex
11
+ return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
12
+ }
13
+
14
+ // Helper to check if any console.error call contains a string (after stripping ANSI codes)
15
+ function expectErrorContaining(
16
+ spy: ReturnType<typeof vi.spyOn>,
17
+ substring: string,
18
+ ) {
19
+ const calls = spy.mock.calls.map((call) => stripAnsi(String(call[0])));
20
+ expect(calls.some((msg) => msg.includes(substring))).toBe(true);
21
+ }
22
+
23
+ describe("printRenderStack", () => {
24
+ let originalEnv: string | undefined;
25
+
26
+ beforeEach(() => {
27
+ // Enable debug mode
28
+ originalEnv = process.env.ALLOY_DEBUG;
29
+ process.env.ALLOY_DEBUG = "1";
30
+ });
31
+
32
+ afterEach(() => {
33
+ // Restore environment
34
+ if (originalEnv === undefined) {
35
+ delete process.env.ALLOY_DEBUG;
36
+ } else {
37
+ process.env.ALLOY_DEBUG = originalEnv;
38
+ }
39
+
40
+ // Clear render stack to prevent state leakage between tests
41
+ clearRenderStack();
42
+ });
43
+
44
+ it("prints the current file when an error occurs", () => {
45
+ const consoleErrorSpy = vi.spyOn(console, "error");
46
+
47
+ function ThrowingComponent() {
48
+ throw new Error("Test error");
49
+ }
50
+
51
+ function ParentComponent() {
52
+ return <ThrowingComponent />;
53
+ }
54
+
55
+ expect(() => {
56
+ renderTree(
57
+ <Output>
58
+ <SourceFile path="test.ts" filetype="typescript">
59
+ <ParentComponent />
60
+ </SourceFile>
61
+ </Output>,
62
+ );
63
+ }).toThrow("Test error");
64
+
65
+ // Check that console.error was called with file path
66
+ expectErrorContaining(consoleErrorSpy, "Error rendering in file test.ts");
67
+ expectErrorContaining(consoleErrorSpy, "ParentComponent");
68
+ expectErrorContaining(consoleErrorSpy, "ThrowingComponent");
69
+
70
+ consoleErrorSpy.mockRestore();
71
+ });
72
+
73
+ it("prints joined path from nested directories", () => {
74
+ const consoleErrorSpy = vi.spyOn(console, "error");
75
+
76
+ function ThrowingComponent() {
77
+ throw new Error("Nested error");
78
+ }
79
+
80
+ expect(() => {
81
+ renderTree(
82
+ <Output>
83
+ <SourceDirectory path="dir1">
84
+ <SourceDirectory path="dir2">
85
+ <SourceFile path="test.ts" filetype="typescript">
86
+ <ThrowingComponent />
87
+ </SourceFile>
88
+ </SourceDirectory>
89
+ </SourceDirectory>
90
+ </Output>,
91
+ );
92
+ }).toThrow("Nested error");
93
+
94
+ // Should show the joined path of all directories
95
+ expectErrorContaining(
96
+ consoleErrorSpy,
97
+ "Error rendering in file dir1/dir2/test.ts",
98
+ );
99
+
100
+ consoleErrorSpy.mockRestore();
101
+ });
102
+
103
+ it("works when no file context is present", () => {
104
+ const consoleErrorSpy = vi.spyOn(console, "error");
105
+
106
+ function ThrowingComponent() {
107
+ throw new Error("No file context error");
108
+ }
109
+
110
+ // Track the number of calls before our test
111
+ const callsBefore = consoleErrorSpy.mock.calls.length;
112
+
113
+ expect(() => {
114
+ renderTree(
115
+ <Output>
116
+ <ThrowingComponent />
117
+ </Output>,
118
+ );
119
+ }).toThrow("No file context error");
120
+
121
+ // Get only the calls from THIS test (after callsBefore)
122
+ const callsFromThisTest = consoleErrorSpy.mock.calls.slice(callsBefore);
123
+ const messagesFromThisTest = callsFromThisTest.map((call: any) =>
124
+ stripAnsi(String(call[0])),
125
+ );
126
+
127
+ // Output component creates a SourceDirectory with path "./"
128
+ // The error message should be "Error rendering in file ./"
129
+ expect(
130
+ messagesFromThisTest.some(
131
+ (msg: string) => msg && msg.includes("Error rendering in file ./"),
132
+ ),
133
+ ).toBe(true);
134
+
135
+ consoleErrorSpy.mockRestore();
136
+ });
137
+
138
+ it("includes component stack with props", () => {
139
+ const consoleErrorSpy = vi.spyOn(console, "error");
140
+
141
+ function ThrowingComponent(props: { message: string; count: number }) {
142
+ throw new Error("Component error");
143
+ }
144
+
145
+ function WrapperComponent(props: { value: string }) {
146
+ return <ThrowingComponent message={props.value} count={42} />;
147
+ }
148
+
149
+ expect(() => {
150
+ renderTree(
151
+ <Output>
152
+ <SourceFile path="props-test.ts" filetype="typescript">
153
+ <WrapperComponent value="test" />
154
+ </SourceFile>
155
+ </Output>,
156
+ );
157
+ }).toThrow("Component error");
158
+
159
+ expectErrorContaining(
160
+ consoleErrorSpy,
161
+ "Error rendering in file props-test.ts",
162
+ );
163
+ expectErrorContaining(consoleErrorSpy, "WrapperComponent");
164
+ expectErrorContaining(consoleErrorSpy, 'value: "test"');
165
+ expectErrorContaining(consoleErrorSpy, "ThrowingComponent");
166
+ expectErrorContaining(consoleErrorSpy, 'message: "test", count: 42');
167
+
168
+ consoleErrorSpy.mockRestore();
169
+ });
170
+
171
+ it("prints 'Error rendering:' when no file or directory context is present", () => {
172
+ const consoleErrorSpy = vi.spyOn(console, "error");
173
+
174
+ function ThrowingComponent() {
175
+ throw new Error("No context error");
176
+ }
177
+
178
+ // Track the number of calls before our test
179
+ const callsBefore = consoleErrorSpy.mock.calls.length;
180
+
181
+ // Don't use Output wrapper to avoid SourceDirectory context
182
+ expect(() => {
183
+ renderTree(<ThrowingComponent />);
184
+ }).toThrow();
185
+
186
+ // Get only the calls from THIS test (after callsBefore)
187
+ const callsFromThisTest = consoleErrorSpy.mock.calls.slice(callsBefore);
188
+ const messagesFromThisTest = callsFromThisTest.map((call: any) =>
189
+ stripAnsi(String(call[0])),
190
+ );
191
+
192
+ // Should have "Error rendering:" without file path
193
+ expect(
194
+ messagesFromThisTest.some(
195
+ (msg: string) => msg && msg.includes("Error rendering:"),
196
+ ),
197
+ ).toBe(true);
198
+ // Should NOT have any message with "in file"
199
+ expect(
200
+ messagesFromThisTest.some(
201
+ (msg: string) => msg && msg.includes("in file"),
202
+ ),
203
+ ).toBe(false);
204
+
205
+ consoleErrorSpy.mockRestore();
206
+ });
207
+
208
+ it("shows context name for named context providers", () => {
209
+ const consoleErrorSpy = vi.spyOn(console, "error");
210
+
211
+ const MyContext = createNamedContext<string>("MyContext");
212
+
213
+ function ThrowingComponent() {
214
+ throw new Error("Context error");
215
+ }
216
+
217
+ expect(() => {
218
+ renderTree(
219
+ <Output>
220
+ <SourceFile path="context-test.ts" filetype="typescript">
221
+ <MyContext.Provider value="test-value">
222
+ <ThrowingComponent />
223
+ </MyContext.Provider>
224
+ </SourceFile>
225
+ </Output>,
226
+ );
227
+ }).toThrow("Context error");
228
+
229
+ // Check that the named context provider is shown as a separate component
230
+ expectErrorContaining(consoleErrorSpy, "at MyContext");
231
+ expectErrorContaining(consoleErrorSpy, 'value: "test-value"');
232
+
233
+ consoleErrorSpy.mockRestore();
234
+ });
235
+ });