Skip to content

Commit e2ecc1f

Browse files
authored
chore(compiler): unit tests (#1132)
* chore(react): unit tests * chore(compiler): unit tests
1 parent ca11c5f commit e2ecc1f

29 files changed

Lines changed: 1468 additions & 8 deletions
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2+
import * as path from "path";
3+
4+
// ESM mocks for internal modules used by _loader-utils
5+
vi.mock("./utils/module-params", () => {
6+
return {
7+
parseParametrizedModuleId: vi.fn((rawId: string) => {
8+
const url = new URL(rawId, "module://");
9+
return {
10+
id: url.pathname.replace(/^\//, ""),
11+
params: Object.fromEntries(url.searchParams.entries()),
12+
};
13+
}),
14+
};
15+
});
16+
17+
vi.mock("./lib/lcp", () => {
18+
return {
19+
LCP: {
20+
ready: vi.fn(async () => undefined),
21+
getInstance: vi.fn(() => ({ data: { version: 0.1 } })),
22+
},
23+
};
24+
});
25+
26+
vi.mock("./lib/lcp/server", () => {
27+
return {
28+
LCPServer: {
29+
loadDictionaries: vi.fn(async () => ({})),
30+
},
31+
};
32+
});
33+
34+
// Import under test AFTER mocks
35+
import { loadDictionary, transformComponent } from "./_loader-utils";
36+
import { defaultParams } from "./_base";
37+
38+
describe("loadDictionary", () => {
39+
beforeEach(async () => {
40+
const lcpMod = await import("./lib/lcp");
41+
(lcpMod.LCP.ready as any).mockClear();
42+
(lcpMod.LCP.getInstance as any).mockClear();
43+
const serverMod = await import("./lib/lcp/server");
44+
(serverMod.LCPServer.loadDictionaries as any).mockClear();
45+
});
46+
47+
it("returns null when path is not a dictionary file", async () => {
48+
const result = await loadDictionary({
49+
resourcePath: "/project/src/lingo/not-dictionary.tsx",
50+
resourceQuery: "",
51+
params: {},
52+
sourceRoot: "src",
53+
lingoDir: "lingo",
54+
isDev: false,
55+
});
56+
expect(result).toBeNull();
57+
const lcpMod = await import("./lib/lcp");
58+
expect(lcpMod.LCP.ready).not.toHaveBeenCalled();
59+
});
60+
61+
it("returns null when locale param is missing", async () => {
62+
// Override parser to drop locale
63+
const parseMod = await import("./utils/module-params");
64+
(parseMod.parseParametrizedModuleId as any).mockImplementation(
65+
(rawId: string) => ({ id: rawId, params: {} }),
66+
);
67+
68+
const result = await loadDictionary({
69+
resourcePath: "/project/src/lingo/dictionary.js",
70+
resourceQuery: "",
71+
params: {},
72+
sourceRoot: "src",
73+
lingoDir: "lingo",
74+
isDev: false,
75+
});
76+
expect(result).toBeNull();
77+
const lcpMod = await import("./lib/lcp");
78+
expect(lcpMod.LCP.ready).not.toHaveBeenCalled();
79+
});
80+
81+
it("loads dictionary for provided locale and passes params to server", async () => {
82+
// Restore default module param parser
83+
const parseMod = await import("./utils/module-params");
84+
(parseMod.parseParametrizedModuleId as any).mockImplementation(
85+
(rawId: string) => {
86+
const url = new URL(rawId, "module://");
87+
return {
88+
id: url.pathname.replace(/^\//, ""),
89+
params: Object.fromEntries(url.searchParams.entries()),
90+
};
91+
},
92+
);
93+
94+
const DICT = { version: 0.1, locale: "es", files: {} };
95+
const serverMod = await import("./lib/lcp/server");
96+
(serverMod.LCPServer.loadDictionaries as any).mockResolvedValueOnce({
97+
es: DICT,
98+
});
99+
100+
const result = await loadDictionary({
101+
resourcePath: "/project/src/lingo/dictionary.js",
102+
resourceQuery: "?locale=es",
103+
params: { sourceLocale: "en", targetLocales: ["es"], foo: "bar" },
104+
sourceRoot: "src",
105+
lingoDir: "lingo",
106+
isDev: true,
107+
});
108+
109+
expect(result).toEqual(DICT);
110+
const lcpMod = await import("./lib/lcp");
111+
expect(lcpMod.LCP.ready).toHaveBeenCalledWith({
112+
sourceRoot: "src",
113+
lingoDir: "lingo",
114+
isDev: true,
115+
});
116+
expect(lcpMod.LCP.getInstance).toHaveBeenCalledWith({
117+
sourceRoot: "src",
118+
lingoDir: "lingo",
119+
isDev: true,
120+
});
121+
expect(serverMod.LCPServer.loadDictionaries).toHaveBeenCalledWith({
122+
sourceLocale: "en",
123+
targetLocales: ["es"],
124+
foo: "bar",
125+
lcp: { version: 0.1 },
126+
});
127+
});
128+
129+
it("throws when dictionary for locale is missing", async () => {
130+
const serverMod = await import("./lib/lcp/server");
131+
(serverMod.LCPServer.loadDictionaries as any).mockResolvedValueOnce({});
132+
await expect(
133+
loadDictionary({
134+
resourcePath: "/project/src/lingo/dictionary.js",
135+
resourceQuery: "?locale=fr",
136+
params: { sourceLocale: "en", targetLocales: ["fr"] },
137+
sourceRoot: "src",
138+
lingoDir: "lingo",
139+
isDev: false,
140+
}),
141+
).rejects.toThrow('Dictionary for locale "fr" could not be generated.');
142+
});
143+
});
144+
145+
describe("transformComponent", () => {
146+
it("returns the same code when nothing to transform and normalizes relativeFilePath", () => {
147+
const code = "export const X = 1;";
148+
const result = transformComponent({
149+
code,
150+
params: defaultParams,
151+
resourcePath: path.join("/project", "src", "deep", "file.tsx"),
152+
sourceRoot: "src",
153+
});
154+
expect(result.code).toContain("export const X = 1;");
155+
// sanity: should return a code+map object
156+
expect(result.map).toBeDefined();
157+
});
158+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
import compiler from "./index";
3+
4+
// Silence logs in tests
5+
vi.spyOn(console, "log").mockImplementation(() => undefined as any);
6+
vi.spyOn(console, "warn").mockImplementation(() => undefined as any);
7+
8+
vi.mock("./utils/env", () => ({ isRunningInCIOrDocker: () => true }));
9+
vi.mock("./lib/lcp/cache", () => ({
10+
LCPCache: { ensureDictionaryFile: vi.fn() },
11+
}));
12+
vi.mock("unplugin", () => ({
13+
createUnplugin: () => ({
14+
vite: vi.fn(() => ({ name: "test-plugin" })),
15+
webpack: vi.fn(() => ({ name: "test-plugin" })),
16+
}),
17+
}));
18+
19+
describe("compiler integration", () => {
20+
beforeEach(() => {
21+
(process as any).env = { ...process.env };
22+
});
23+
24+
it("next() returns a function and sets webpack wrapper when turbopack disabled", () => {
25+
const cfg: any = { webpack: (c: any) => c };
26+
const out = compiler.next({
27+
sourceRoot: "src",
28+
models: "lingo.dev",
29+
turbopack: { enabled: false },
30+
})(cfg);
31+
expect(typeof out.webpack).toBe("function");
32+
});
33+
34+
it("vite() pushes plugin to front and detects framework label", () => {
35+
const cfg: any = { plugins: [{ name: "react-router" }] };
36+
const out = compiler.vite({})(cfg);
37+
expect(out.plugins[0]).toBeDefined();
38+
});
39+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
import { createPayload, createOutput, defaultParams } from "./_base";
3+
import { jsxAttributeScopesExportMutation } from "./jsx-attribute-scopes-export";
4+
5+
vi.mock("./lib/lcp", () => {
6+
const instance = {
7+
resetScope: vi.fn().mockReturnThis(),
8+
setScopeType: vi.fn().mockReturnThis(),
9+
setScopeHash: vi.fn().mockReturnThis(),
10+
setScopeContext: vi.fn().mockReturnThis(),
11+
setScopeSkip: vi.fn().mockReturnThis(),
12+
setScopeOverrides: vi.fn().mockReturnThis(),
13+
setScopeContent: vi.fn().mockReturnThis(),
14+
save: vi.fn(),
15+
};
16+
const getInstance = vi.fn(() => instance);
17+
return {
18+
LCP: {
19+
getInstance,
20+
},
21+
__test__: { instance, getInstance },
22+
};
23+
});
24+
describe("jsxAttributeScopesExportMutation", () => {
25+
beforeEach(() => {
26+
// dynamic import avoids ESM mock timing issues
27+
return import("./lib/lcp").then((lcpMod) => {
28+
(lcpMod.LCP.getInstance as any).mockClear();
29+
});
30+
});
31+
32+
it("collects attribute scopes and saves to LCP", async () => {
33+
const code = `
34+
export default function X() {
35+
return <div data-jsx-attribute-scope="title:scope-1" title="Hello"/>;
36+
}`.trim();
37+
const input = createPayload({
38+
code,
39+
params: defaultParams,
40+
relativeFilePath: "src/App.tsx",
41+
} as any);
42+
const out = jsxAttributeScopesExportMutation(input);
43+
// Not asserting output code as mutation does not change AST; assert side effects
44+
const lcpMod: any = await import("./lib/lcp");
45+
const inst = lcpMod.__test__.instance;
46+
expect(lcpMod.LCP.getInstance).toHaveBeenCalled();
47+
expect(inst.setScopeType).toHaveBeenCalledWith(
48+
"src/App.tsx",
49+
"scope-1",
50+
"attribute",
51+
);
52+
expect(inst.setScopeContent).toHaveBeenCalledWith(
53+
"src/App.tsx",
54+
"scope-1",
55+
"Hello",
56+
);
57+
expect(inst.save).toHaveBeenCalled();
58+
});
59+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { describe, it, expect } from "vitest";
2+
import { createPayload, createOutput, defaultParams } from "./_base";
3+
import { jsxHtmlLangMutation } from "./jsx-html-lang";
4+
5+
function run(code: string, rsc = true) {
6+
const input = createPayload({
7+
code,
8+
params: { ...defaultParams, rsc },
9+
relativeFilePath: "app/layout.tsx",
10+
} as any);
11+
const mutated = jsxHtmlLangMutation(input);
12+
return createOutput(mutated!).code.trim();
13+
}
14+
15+
describe("jsxHtmlLangMutation", () => {
16+
it("replaces html tag with framework component in server mode", () => {
17+
const input = `
18+
export default function Root() {
19+
return <html><body>Hi</body></html>
20+
}`.trim();
21+
const out = run(input, true);
22+
expect(out).toMatch(/LingoHtmlComponent/);
23+
});
24+
25+
it("replaces html tag with framework component in client mode", () => {
26+
const input = `
27+
"use client";
28+
export default function Root() {
29+
return <html><body>Hi</body></html>
30+
}`.trim();
31+
const out = run(input, false);
32+
expect(out).toMatch(/LingoHtmlComponent/);
33+
});
34+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, it, expect } from "vitest";
2+
import { createPayload, createOutput, defaultParams } from "./_base";
3+
import jsxProviderMutation from "./jsx-provider";
4+
5+
function run(code: string, rsc = true) {
6+
const input = createPayload({
7+
code,
8+
params: { ...defaultParams, rsc },
9+
relativeFilePath: "app/layout.tsx",
10+
} as any);
11+
const mutated = jsxProviderMutation(input);
12+
return createOutput(mutated!).code.trim();
13+
}
14+
15+
describe("jsxProviderMutation", () => {
16+
it("wraps <html> with LingoProvider in server mode", () => {
17+
const input = `
18+
export default function Root() {
19+
return <html><body>Hi</body></html>
20+
}`.trim();
21+
const out = run(input, true);
22+
expect(out).toContain("LingoProvider");
23+
expect(out).toContain("loadDictionary");
24+
});
25+
26+
it("does not modify in client mode", () => {
27+
const input = `
28+
export default function Root() {
29+
return <html><body>Hi</body></html>
30+
}`.trim();
31+
const out = run(input, false);
32+
expect(out).toContain("<html>");
33+
expect(out).not.toContain("LingoProvider");
34+
});
35+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
import { createPayload, defaultParams } from "./_base";
3+
import { jsxScopesExportMutation } from "./jsx-scopes-export";
4+
5+
vi.mock("./lib/lcp", () => {
6+
const instance = {
7+
resetScope: vi.fn().mockReturnThis(),
8+
setScopeType: vi.fn().mockReturnThis(),
9+
setScopeHash: vi.fn().mockReturnThis(),
10+
setScopeContext: vi.fn().mockReturnThis(),
11+
setScopeSkip: vi.fn().mockReturnThis(),
12+
setScopeOverrides: vi.fn().mockReturnThis(),
13+
setScopeContent: vi.fn().mockReturnThis(),
14+
save: vi.fn(),
15+
};
16+
const getInstance = vi.fn(() => instance);
17+
return {
18+
LCP: {
19+
getInstance,
20+
},
21+
__test__: { instance, getInstance },
22+
};
23+
});
24+
25+
describe("jsxScopesExportMutation", () => {
26+
beforeEach(() => {
27+
vi.clearAllMocks();
28+
});
29+
30+
it("exports element scope with hash/content/flags", async () => {
31+
const code = `
32+
export default function X(){
33+
return <div data-jsx-scope="scope-1">Foobar</div>
34+
}`.trim();
35+
const input = createPayload({
36+
code,
37+
params: defaultParams,
38+
relativeFilePath: "src/App.tsx",
39+
} as any);
40+
jsxScopesExportMutation(input);
41+
const lcpMod: any = await import("./lib/lcp");
42+
const inst = lcpMod.__test__.instance;
43+
expect(lcpMod.LCP.getInstance).toHaveBeenCalled();
44+
expect(inst.setScopeType).toHaveBeenCalledWith(
45+
"src/App.tsx",
46+
"0/declaration/body/0/argument",
47+
"element",
48+
);
49+
expect(inst.setScopeContent).toHaveBeenCalledWith(
50+
"src/App.tsx",
51+
"0/declaration/body/0/argument",
52+
"Foobar",
53+
);
54+
expect(inst.save).toHaveBeenCalled();
55+
});
56+
});

0 commit comments

Comments
 (0)