Skip to content

Commit 5bfefad

Browse files
committed
test(playground): expand bundlePreview, codemirror, and state tests
1 parent d2751b5 commit 5bfefad

4 files changed

Lines changed: 137 additions & 3 deletions

File tree

apps/playground/src/pipeline/bundlePreview.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,15 +198,18 @@ try {
198198
},
199199
bundle: true,
200200
write: false,
201+
// An out dir is required for esbuild to emit CSS from user
202+
// `import './styles.css'`; without it such imports fail to build.
203+
outdir: "/",
201204
format: "esm",
202205
jsx: "automatic",
203206
jsxDev: false,
204207
target: "es2022",
205208
plugins: [createVirtualFsPlugin(input, reactVendor, runtimeSrc)],
206209
});
207210

208-
// esbuild names the stdin JS output `<stdout>`; CSS from user imports lands
209-
// in a sibling `.css` file.
211+
// The JS bundle and any CSS from user imports are emitted as sibling
212+
// outputs; the CSS file is the one whose path ends in `.css`.
210213
const bundleJs =
211214
result.outputFiles.find((file) => !file.path.endsWith(".css"))?.text ?? "";
212215
const css = result.outputFiles

apps/playground/test/bundlePreview.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,56 @@ describe("bundlePreview", () => {
4747
expect(result.reactIife).toBe("globalThis.PGReactVendor = {};");
4848
});
4949

50+
it("loads every source extension and extracts user CSS", async () => {
51+
const { bundlePreview } = await import("@/pipeline/bundlePreview");
52+
53+
const result = await bundlePreview({
54+
files: {
55+
"styleframe.config.ts": "export default {};",
56+
"App.tsx": [
57+
'import "./styles.css";',
58+
'import "virtual:styleframe.css";',
59+
'import { tsHelper } from "./helper";',
60+
'import { jsUtil } from "./util";',
61+
'import Widget from "./widget";',
62+
"export default function App(){ return <div>{tsHelper}{jsUtil}<Widget/></div>; }",
63+
].join("\n"),
64+
"helper.ts": 'export const tsHelper = "PG_TS_VALUE";',
65+
"util.js": 'export const jsUtil = "PG_JS_VALUE";',
66+
"widget.jsx":
67+
"export default function Widget(){ return <span>w</span>; }",
68+
"styles.css": ".pg-user { color: red; }",
69+
},
70+
entryPath: "App.tsx",
71+
configPath: "styleframe.config.ts",
72+
runtimeTs: "",
73+
});
74+
75+
// The .ts, .js and .jsx modules are each loaded and inlined.
76+
expect(result.bundleJs).toContain("PG_TS_VALUE");
77+
expect(result.bundleJs).toContain("PG_JS_VALUE");
78+
expect(result.bundleJs).toContain("function Widget");
79+
// The imported .css file is extracted into the css output, while
80+
// virtual:styleframe.css resolves to an empty module.
81+
expect(result.css).toContain(".pg-user");
82+
});
83+
84+
it("rejects an unknown bare import", async () => {
85+
const { bundlePreview } = await import("@/pipeline/bundlePreview");
86+
87+
await expect(
88+
bundlePreview({
89+
files: {
90+
"App.tsx":
91+
'import _ from "lodash";\nexport default function App(){ return <div>{String(_)}</div>; }',
92+
},
93+
entryPath: "App.tsx",
94+
configPath: "styleframe.config.ts",
95+
runtimeTs: "",
96+
}),
97+
).rejects.toThrow();
98+
});
99+
50100
it("rejects when a relative import cannot be resolved", async () => {
51101
const { bundlePreview } = await import("@/pipeline/bundlePreview");
52102

apps/playground/test/codemirror.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
// @vitest-environment jsdom
22
import type { EditorView } from "@codemirror/view";
33
import { afterEach, describe, expect, it, vi } from "vitest";
4-
import { createEditor, setEditorValue } from "@/editor/codemirror";
4+
import {
5+
createEditor,
6+
getEditorSelection,
7+
languageForPath,
8+
setEditorValue,
9+
} from "@/editor/codemirror";
510

611
const openEditors: EditorView[] = [];
712

@@ -73,6 +78,37 @@ describe("createEditor", () => {
7378
});
7479
});
7580

81+
describe("languageForPath", () => {
82+
it("maps file extensions to editor languages", () => {
83+
expect(languageForPath("styles.css")).toBe("css");
84+
expect(languageForPath("App.tsx")).toBe("tsx");
85+
expect(languageForPath("Widget.jsx")).toBe("tsx");
86+
expect(languageForPath("config.ts")).toBe("typescript");
87+
// Extensionless paths fall back to TypeScript.
88+
expect(languageForPath("README")).toBe("typescript");
89+
});
90+
});
91+
92+
describe("getEditorSelection", () => {
93+
it("reports the 1-based line and column of the cursor", () => {
94+
const view = mount({ doc: "ab\ncd" });
95+
// Position 4 is the second column of the second line ("c|d").
96+
view.dispatch({ selection: { anchor: 4 } });
97+
98+
expect(getEditorSelection(view)).toEqual({ line: 2, column: 2 });
99+
});
100+
101+
it("invokes onSelectionChange when the selection moves", () => {
102+
const onSelectionChange = vi.fn();
103+
const view = mount({ doc: "hello", onSelectionChange });
104+
onSelectionChange.mockClear();
105+
106+
view.dispatch({ selection: { anchor: 3 } });
107+
108+
expect(onSelectionChange).toHaveBeenCalledWith({ line: 1, column: 4 });
109+
});
110+
});
111+
76112
describe("setEditorValue", () => {
77113
it("dispatches a replacement when the value differs", () => {
78114
const view = mount({ doc: "old" });

apps/playground/test/playgroundState.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
normalizePath,
1111
openFile,
1212
renameFile,
13+
setActivePath,
1314
usePlaygroundSnapshot,
1415
usePlaygroundState,
1516
} from "@/state/playground";
@@ -133,11 +134,41 @@ describe("createFile", () => {
133134
deleteFile("Widget.tsx");
134135
});
135136

137+
it("seeds non-template files with empty content", () => {
138+
const s = usePlaygroundState();
139+
// A plain `.ts` (non-`.styleframe.ts`) file gets no template.
140+
expect(createFile("src/helpers/format.ts")).toBe(true);
141+
expect(s.files["src/helpers/format.ts"]).toBe("");
142+
// `.css` files are likewise empty.
143+
expect(createFile("src/helpers/reset.css")).toBe(true);
144+
expect(s.files["src/helpers/reset.css"]).toBe("");
145+
deleteFile("src/helpers/format.ts");
146+
deleteFile("src/helpers/reset.css");
147+
});
148+
149+
it("prefixes a component name that does not start with a letter", () => {
150+
const s = usePlaygroundState();
151+
expect(createFile("9lives.tsx")).toBe(true);
152+
expect(s.files["9lives.tsx"]).toContain("function Component9lives");
153+
deleteFile("9lives.tsx");
154+
});
155+
136156
function state() {
137157
return usePlaygroundState();
138158
}
139159
});
140160

161+
describe("setActivePath", () => {
162+
it("activates an existing file and ignores unknown paths", () => {
163+
const s = usePlaygroundState();
164+
setActivePath("src/App.tsx");
165+
expect(s.activePath).toBe("src/App.tsx");
166+
167+
setActivePath("does/not/exist.tsx");
168+
expect(s.activePath).toBe("src/App.tsx");
169+
});
170+
});
171+
141172
describe("renameFile", () => {
142173
it("moves content and protects the config and entry files", () => {
143174
const s = usePlaygroundState();
@@ -209,6 +240,20 @@ describe("deleteFolder", () => {
209240
expect(deleteFolder("src")).toBe(false);
210241
expect(s.files[ENTRY_PATH]).toBeDefined();
211242
});
243+
244+
it("prunes explicit folders nested under the deleted folder", () => {
245+
const s = usePlaygroundState();
246+
createFile("src/blocks/Hero/Hero.tsx");
247+
createFolder("src/blocks/empty");
248+
249+
expect(deleteFolder("src/blocks")).toBe(true);
250+
expect(s.files["src/blocks/Hero/Hero.tsx"]).toBeUndefined();
251+
expect(s.folders).not.toContain("src/blocks/empty");
252+
});
253+
254+
it("rejects a blank folder path", () => {
255+
expect(deleteFolder(" ")).toBe(false);
256+
});
212257
});
213258

214259
describe("normalizePath", () => {

0 commit comments

Comments
 (0)