vitest-browser-qwik 0.0.7 → 0.0.9

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 CHANGED
@@ -5,7 +5,7 @@ A modern testing setup demonstrating browser-based testing for Qwik components u
5
5
  ## Getting Started
6
6
 
7
7
  ```bash
8
- pnpm add vitest-browser-qwik
8
+ npm install -D vitest-browser-qwik
9
9
  ```
10
10
 
11
11
  ## Core Features
@@ -15,6 +15,27 @@ pnpm add vitest-browser-qwik
15
15
  - **`renderHook`** - Hook testing utilities (currently only CSR supported)
16
16
  - All functions are async for predictable testing behavior
17
17
 
18
+ ## Vitest Config Setup
19
+
20
+ ```tsx
21
+ import { defineConfig } from 'vitest/config'
22
+ import { qwikVite } from '@builder.io/qwik/optimizer'
23
+
24
+ // optional, run the tests in SSR mode
25
+ import { testSSR } from 'vitest-browser-qwik/ssr-plugin'
26
+
27
+ export default defineConfig({
28
+ plugins: [testSSR(), qwikVite()],
29
+ test: {
30
+ browser: {
31
+ enabled: true,
32
+ provider: 'playwright',
33
+ instances: [{ browser: 'chromium' }]
34
+ },
35
+ },
36
+ })
37
+ ```
38
+
18
39
  ### Client-Side Rendering Example
19
40
 
20
41
  ```tsx
@@ -113,6 +134,10 @@ test('renders with custom container', async () => {
113
134
  - **SSR Context**: `renderSSR` executes components in a Node.js context separate from your test files, providing true server-side rendering simulation
114
135
  - **Same Interface**: Both CSR and SSR provide the same testing interface, making it easy to test both rendering modes
115
136
 
137
+ ## Compatibility
138
+ In testing, we have observed render issues with Vite 5.x. We recommend using Vite 6+. Qwik 1 currently specifies Vite 5.x,
139
+ but Vite 6.x should work as well.
140
+
116
141
  ## Limitations
117
142
 
118
143
  - For `renderSSR` you must always import the component from another file, local components are not supported. This is because this would require importing the vitest context, or moving local components into separate files dynamically, which involves a lot of unwanted complexity.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { RenderResult, SSRRenderOptions, cleanup$1 as cleanup, render$1 as render, renderHook$1 as renderHook, renderServerHTML$1 as renderServerHTML } from "./pure-CoHVQ6I9.js";
1
+ import { RenderResult, SSRRenderOptions, cleanup$1 as cleanup, render$1 as render, renderHook$1 as renderHook, renderServerHTML$1 as renderServerHTML } from "./pure-Cwz8HPNP.js";
2
2
  import { JSXOutput } from "@builder.io/qwik";
3
3
 
4
4
  //#region src/index.d.ts
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { cleanup, render, renderHook, renderServerHTML } from "./pure-CGnRrG6t.js";
1
+ import { cleanup, render, renderHook, renderServerHTML } from "./pure-BWRD5UQA.js";
2
2
  import { page } from "@vitest/browser/context";
3
3
  import { beforeEach } from "vitest";
4
4
 
@@ -8,8 +8,8 @@ page.extend({
8
8
  renderServerHTML,
9
9
  [Symbol.for("vitest:component-cleanup")]: cleanup
10
10
  });
11
- beforeEach(() => {
12
- cleanup();
11
+ beforeEach(async () => {
12
+ await cleanup();
13
13
  });
14
14
 
15
15
  //#endregion
@@ -83,7 +83,7 @@ async function renderHook(hook) {
83
83
  }
84
84
  };
85
85
  }
86
- function cleanup() {
86
+ async function cleanup() {
87
87
  mountedContainers.forEach((container) => {
88
88
  container.innerHTML = "";
89
89
  if (container.parentNode === document.body) document.body.removeChild(container);
@@ -31,6 +31,6 @@ interface RenderHookResult<Result> {
31
31
  unmount: () => void;
32
32
  }
33
33
  declare function renderHook<Result>(hook: () => Result): Promise<RenderHookResult<Result>>;
34
- declare function cleanup(): void;
34
+ declare function cleanup(): Promise<void>;
35
35
  //#endregion
36
36
  export { RenderHookResult, RenderOptions, RenderResult, SSRRenderOptions, cleanup as cleanup$1, render$1, renderHook as renderHook$1, renderServerHTML as renderServerHTML$1 };
package/dist/pure.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { RenderHookResult, RenderOptions, RenderResult, SSRRenderOptions, cleanup$1 as cleanup, render$1 as render, renderHook$1 as renderHook, renderServerHTML$1 as renderServerHTML } from "./pure-CoHVQ6I9.js";
1
+ import { RenderHookResult, RenderOptions, RenderResult, SSRRenderOptions, cleanup$1 as cleanup, render$1 as render, renderHook$1 as renderHook, renderServerHTML$1 as renderServerHTML } from "./pure-Cwz8HPNP.js";
2
2
  export { RenderHookResult, RenderOptions, RenderResult, SSRRenderOptions, cleanup, render, renderHook, renderServerHTML };
package/dist/pure.js CHANGED
@@ -1,3 +1,3 @@
1
- import { cleanup, render, renderHook, renderServerHTML } from "./pure-CGnRrG6t.js";
1
+ import { cleanup, render, renderHook, renderServerHTML } from "./pure-BWRD5UQA.js";
2
2
 
3
3
  export { cleanup, render, renderHook, renderServerHTML };
@@ -0,0 +1,6 @@
1
+ import { Plugin } from "vitest/config";
2
+
3
+ //#region src/ssr-plugin.d.ts
4
+ declare function testSSR(): Plugin;
5
+ //#endregion
6
+ export { testSSR };
@@ -0,0 +1,248 @@
1
+ import { dirname, relative, resolve } from "node:path";
2
+ import { symbolMapper } from "@builder.io/qwik/optimizer";
3
+
4
+ //#region src/ssr-plugin.ts
5
+ function traverseChildren(node, callback) {
6
+ for (const key in node) {
7
+ const child = node[key];
8
+ if (Array.isArray(child)) {
9
+ for (const item of child) if (item && typeof item === "object" && callback(item)) return true;
10
+ } else if (child && typeof child === "object") {
11
+ if (callback(child)) return true;
12
+ }
13
+ }
14
+ return false;
15
+ }
16
+ async function hasRenderSSRCall(code, filename) {
17
+ try {
18
+ const { parseSync } = await import("oxc-parser");
19
+ const ast = parseSync(filename, code);
20
+ const renderSSRIdentifiers = new Set(["renderSSR"]);
21
+ let hasRenderSSRCallInCode = false;
22
+ function walkForDetection(node) {
23
+ if (!node || typeof node !== "object") return false;
24
+ if (node.type === "ImportDeclaration") {
25
+ const importDecl = node;
26
+ if (!importDecl.source?.value || !importDecl.specifiers) return false;
27
+ for (const spec of importDecl.specifiers) if (spec.type === "ImportSpecifier") {
28
+ const importSpec = spec;
29
+ if (importSpec.imported.type !== "Identifier") continue;
30
+ if (importSpec.imported.name === "renderSSR") renderSSRIdentifiers.add(importSpec.local.name);
31
+ } else if (spec.type === "ImportDefaultSpecifier") {
32
+ const defaultSpec = spec;
33
+ if (defaultSpec.local.name.toLowerCase().includes("renderssr")) renderSSRIdentifiers.add(defaultSpec.local.name);
34
+ }
35
+ }
36
+ if (node.type === "FunctionDeclaration") {
37
+ const funcDecl = node;
38
+ if (funcDecl.id?.name === "renderSSR") renderSSRIdentifiers.add("renderSSR");
39
+ }
40
+ if (node.type === "TSDeclareFunction") {
41
+ const declareFunc = node;
42
+ if (declareFunc.id?.name === "renderSSR") renderSSRIdentifiers.add("renderSSR");
43
+ }
44
+ if (node.type === "VariableDeclarator") {
45
+ const varDecl = node;
46
+ if (varDecl.id.type !== "Identifier") return false;
47
+ if (varDecl.init?.type !== "Identifier") return false;
48
+ if (!renderSSRIdentifiers.has(varDecl.init.name)) return false;
49
+ const bindingId = varDecl.id;
50
+ renderSSRIdentifiers.add(bindingId.name);
51
+ }
52
+ if (node.type === "CallExpression") {
53
+ const callExpr = node;
54
+ if (callExpr.callee.type === "Identifier") {
55
+ if (renderSSRIdentifiers.has(callExpr.callee.name)) {
56
+ hasRenderSSRCallInCode = true;
57
+ return true;
58
+ }
59
+ }
60
+ }
61
+ return traverseChildren(node, walkForDetection);
62
+ }
63
+ walkForDetection(ast);
64
+ const hasCallsInString = code.includes("renderSSR(");
65
+ const result = hasRenderSSRCallInCode || hasCallsInString;
66
+ return result;
67
+ } catch (error) {
68
+ console.warn(`Failed to parse ${filename} for renderSSR detection, falling back to string check:`, error);
69
+ return code.includes("renderSSR");
70
+ }
71
+ }
72
+ function resolveComponentPath(importPath, testFileId) {
73
+ if (!importPath.startsWith(".")) return importPath.endsWith(".tsx") || importPath.endsWith(".ts") ? importPath : `${importPath}.tsx`;
74
+ const testFileDir = dirname(testFileId);
75
+ const resolvedPath = resolve(testFileDir, importPath);
76
+ const projectRoot = process.cwd();
77
+ let componentPath = `./${relative(projectRoot, resolvedPath)}`;
78
+ if (!componentPath.endsWith(".tsx") && !componentPath.endsWith(".ts")) componentPath += ".tsx";
79
+ return componentPath;
80
+ }
81
+ function extractPropsFromJSX(attributes, sourceCode) {
82
+ const props = {};
83
+ for (const attr of attributes) {
84
+ if (attr.type !== "JSXAttribute") continue;
85
+ const jsxAttr = attr;
86
+ if (jsxAttr.name.type !== "JSXIdentifier") continue;
87
+ const propName = jsxAttr.name.name;
88
+ if (!jsxAttr.value) continue;
89
+ if (jsxAttr.value.type === "JSXExpressionContainer") {
90
+ const container = jsxAttr.value;
91
+ if (container.expression.type !== "JSXEmptyExpression") {
92
+ const exprSpan = container.expression;
93
+ const expressionCode = sourceCode.slice(exprSpan.start, exprSpan.end);
94
+ props[propName] = expressionCode;
95
+ }
96
+ } else if (jsxAttr.value.type === "Literal") {
97
+ const literal = jsxAttr.value;
98
+ props[propName] = JSON.stringify(literal.value);
99
+ }
100
+ }
101
+ return props;
102
+ }
103
+ function isTestFile(id) {
104
+ return id.includes(".test.") || id.includes(".spec.");
105
+ }
106
+ function hasCommandsImport(node) {
107
+ if (node.type !== "ImportDeclaration") return false;
108
+ const importDecl = node;
109
+ if (importDecl.source?.value !== "@vitest/browser/context") return false;
110
+ if (!importDecl.specifiers) return false;
111
+ return importDecl.specifiers.some((spec) => spec.type === "ImportSpecifier" && spec.imported.type === "Identifier" && spec.imported.name === "commands");
112
+ }
113
+ const renderSSRCommand = async (ctx, componentPath, componentName, props = {}) => {
114
+ try {
115
+ const projectRoot = process.cwd();
116
+ const absoluteComponentPath = resolve(projectRoot, componentPath);
117
+ const viteServer = ctx.project.vite;
118
+ for (const [key, value] of Object.entries(viteServer.config.env)) viteServer.config.define[`__vite_ssr_import_meta__.env.${key}`] = JSON.stringify(value);
119
+ const componentModule = await viteServer.ssrLoadModule(absoluteComponentPath);
120
+ const Component = componentModule[componentName];
121
+ if (!Component) throw new Error(`Component "${componentName}" not found in ${absoluteComponentPath}`);
122
+ const qwikModule = await viteServer.ssrLoadModule("@builder.io/qwik");
123
+ const { jsx } = qwikModule;
124
+ const jsxElement = jsx(Component, props);
125
+ const serverModule = await viteServer.ssrLoadModule("@builder.io/qwik/server");
126
+ const { renderToString } = serverModule;
127
+ const result = await renderToString(jsxElement, {
128
+ containerTagName: "div",
129
+ base: "/",
130
+ qwikLoader: { include: "always" },
131
+ symbolMapper: globalThis.qwikSymbolMapper
132
+ });
133
+ return { html: result.html };
134
+ } catch (error) {
135
+ console.error("SSR Command Error:", error);
136
+ throw error;
137
+ }
138
+ };
139
+ function testSSR() {
140
+ return {
141
+ name: "vitest:ssr-transform",
142
+ enforce: "pre",
143
+ async transform(code, id) {
144
+ if (!isTestFile(id)) return null;
145
+ if (!await hasRenderSSRCall(code, id)) return null;
146
+ try {
147
+ const { parseSync } = await import("oxc-parser");
148
+ const MagicString = (await import("magic-string")).default;
149
+ const ast = parseSync(id, code);
150
+ const s = new MagicString(code);
151
+ const componentImports = new Map();
152
+ const renderSSRIdentifiers = new Set(["renderSSR"]);
153
+ let hasExistingCommandsImport = false;
154
+ function walkForTransformation(node) {
155
+ if (!node || typeof node !== "object") return;
156
+ if (node.type === "ImportDeclaration") {
157
+ const importDecl = node;
158
+ if (importDecl.source?.value && importDecl.specifiers) {
159
+ const source = importDecl.source.value;
160
+ for (const spec of importDecl.specifiers) if (spec.type === "ImportSpecifier") {
161
+ const importSpec = spec;
162
+ if (importSpec.imported.type === "Identifier") {
163
+ componentImports.set(importSpec.imported.name, source);
164
+ if (importSpec.imported.name === "renderSSR") renderSSRIdentifiers.add(importSpec.local.name);
165
+ }
166
+ } else if (spec.type === "ImportDefaultSpecifier") {
167
+ const defaultSpec = spec;
168
+ if (defaultSpec.local.name.toLowerCase().includes("renderssr")) renderSSRIdentifiers.add(defaultSpec.local.name);
169
+ }
170
+ }
171
+ }
172
+ if (node.type === "VariableDeclarator") {
173
+ const varDecl = node;
174
+ if (varDecl.id.type === "Identifier" && varDecl.init?.type === "Identifier" && renderSSRIdentifiers.has(varDecl.init.name)) {
175
+ const bindingId = varDecl.id;
176
+ renderSSRIdentifiers.add(bindingId.name);
177
+ }
178
+ }
179
+ if (hasCommandsImport(node)) hasExistingCommandsImport = true;
180
+ if (node.type === "CallExpression") {
181
+ const callExpr = node;
182
+ if (callExpr.callee.type === "Identifier" && renderSSRIdentifiers.has(callExpr.callee.name)) {
183
+ const jsxArg = callExpr.arguments?.[0];
184
+ if (jsxArg?.type === "JSXElement") {
185
+ const jsxElement = jsxArg;
186
+ if (jsxElement.openingElement?.name?.type === "JSXIdentifier") {
187
+ const componentName = jsxElement.openingElement.name.name;
188
+ const componentImportPath = componentImports.get(componentName);
189
+ if (componentImportPath) {
190
+ const componentPath = resolveComponentPath(componentImportPath, id);
191
+ const props = extractPropsFromJSX(jsxElement.openingElement.attributes || [], code);
192
+ let propsStr = "";
193
+ if (Object.keys(props).length > 0) {
194
+ const propsEntries = Object.entries(props).map(([key, value]) => {
195
+ return `${JSON.stringify(key)}: ${value}`;
196
+ });
197
+ propsStr = `, { ${propsEntries.join(", ")} }`;
198
+ }
199
+ const replacement = `(async () => {
200
+ const { html } = await commands.renderSSR("${componentPath}", "${componentName}"${propsStr});
201
+ return renderServerHTML(html);
202
+ })()`;
203
+ const spanNode = node;
204
+ s.overwrite(spanNode.start, spanNode.end, replacement);
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+ traverseChildren(node, walkForTransformation);
211
+ return;
212
+ }
213
+ walkForTransformation(ast);
214
+ if (!hasExistingCommandsImport && s.hasChanged()) {
215
+ let lastImportEnd = 0;
216
+ function findLastImport(node) {
217
+ if (!node || typeof node !== "object") return;
218
+ if (node.type === "ImportDeclaration") {
219
+ const spanNode = node;
220
+ lastImportEnd = Math.max(lastImportEnd, spanNode.end);
221
+ }
222
+ traverseChildren(node, findLastImport);
223
+ return void 0;
224
+ }
225
+ findLastImport(ast);
226
+ if (lastImportEnd > 0) s.appendLeft(lastImportEnd, "\nimport { commands } from \"@vitest/browser/context\";\nimport { renderServerHTML } from \"vitest-browser-qwik\";");
227
+ }
228
+ if (s.hasChanged()) return {
229
+ code: s.toString(),
230
+ map: s.generateMap({ hires: true })
231
+ };
232
+ } catch (error) {
233
+ console.warn(`Failed to transform ${id}:`, error);
234
+ }
235
+ return null;
236
+ },
237
+ configResolved(config) {
238
+ globalThis.qwikSymbolMapper = symbolMapper;
239
+ if (config.test?.browser?.enabled) config.test.browser.commands = {
240
+ ...config.test.browser.commands,
241
+ renderSSR: renderSSRCommand
242
+ };
243
+ }
244
+ };
245
+ }
246
+
247
+ //#endregion
248
+ export { testSSR };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-browser-qwik",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Render Qwik components using Vitest Browser Mode",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,28 +29,35 @@
29
29
  "./pure": {
30
30
  "types": "./dist/pure.d.ts",
31
31
  "default": "./dist/pure.js"
32
+ },
33
+ "./ssr-plugin": {
34
+ "types": "./dist/ssr-plugin.d.ts",
35
+ "default": "./dist/ssr-plugin.js"
32
36
  }
33
37
  },
34
38
  "publishConfig": {
35
39
  "access": "public"
36
40
  },
41
+ "dependencies": {
42
+ "magic-string": "^0.30.17",
43
+ "oxc-parser": "^0.73.2"
44
+ },
37
45
  "peerDependencies": {
38
46
  "@builder.io/qwik": "^1.14.1",
39
47
  "@vitest/browser": "^3.2.4",
48
+ "vite": ">=6.3.5",
40
49
  "vitest": "^3.1.3"
41
50
  },
42
51
  "devDependencies": {
43
52
  "@biomejs/biome": "2.0.0",
44
53
  "@builder.io/qwik": "1.14.1",
45
54
  "@oxc-project/types": "^0.73.2",
55
+ "@playwright/test": "see flake.nix",
46
56
  "@types/node": "^22.15.17",
47
57
  "@vitest/browser": "^3.2.4",
48
58
  "bumpp": "^10.1.0",
49
- "magic-string": "^0.30.17",
50
- "oxc-parser": "^0.73.2",
51
59
  "oxc-resolver": "^11.2.0",
52
60
  "oxc-walker": "^0.3.0",
53
- "@playwright/test": "see flake.nix",
54
61
  "tsdown": "^0.11.9",
55
62
  "tsx": "^4.19.4",
56
63
  "typescript": "^5.8.3",