Skip to content

Commit 5152c76

Browse files
committed
load Root module before loading the widget module
1 parent 2face6f commit 5152c76

6 files changed

Lines changed: 143 additions & 1 deletion

File tree

spec/fixtures/test-project-with-root/web/chatgpt/WidgetA.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import "./widget.css";
2+
13
export default function WidgetA() {
24
return (
35
<div className="widget-a">
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/* Root layout styles - should be loaded first */
2+
.root-layout {
3+
font-family: system-ui, sans-serif;
4+
min-height: 100vh;
5+
display: flex;
6+
flex-direction: column;
7+
}
8+
9+
.root-header {
10+
background-color: #f0f0f0;
11+
padding: 1rem;
12+
}
13+
14+
.root-content {
15+
flex: 1;
16+
padding: 1rem;
17+
}
18+
19+
.root-footer {
20+
background-color: #f0f0f0;
21+
padding: 1rem;
22+
}
23+

spec/fixtures/test-project-with-root/web/chatgpt/root.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from "react";
2+
import "./root.css";
23

34
export default function RootLayout({ children }: { children: React.ReactNode }) {
45
return (
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/* Widget styles - should be loaded after root styles */
2+
.widget-a {
3+
background-color: #e0e0ff;
4+
padding: 1rem;
5+
border-radius: 8px;
6+
}
7+
8+
.widget-a h2 {
9+
color: #333;
10+
margin-bottom: 0.5rem;
11+
}
12+
13+
.widget-a p {
14+
color: #666;
15+
}
16+

spec/integration/root-layout.spec.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,104 @@ describe("Root Layout Component", () => {
114114
expect(jsContent.length).toBeGreaterThan(100);
115115
});
116116
});
117+
118+
describe("Browser CSS Order Tests", () => {
119+
let devServer: ViteDevServer;
120+
121+
beforeAll(async () => {
122+
devServer = await createServer({
123+
root: FIXTURE_WITH_ROOT_DIR,
124+
configFile: path.join(FIXTURE_WITH_ROOT_DIR, "vite.config.ts"),
125+
server: {
126+
port: 5177,
127+
},
128+
logLevel: "warn",
129+
});
130+
await devServer.listen();
131+
});
132+
133+
afterAll(async () => {
134+
await devServer?.close();
135+
});
136+
137+
it("should inject root layout styles before widget styles in the browser", async () => {
138+
const { content: html } = await getWidgetHTML("WidgetA", { devServer });
139+
140+
// Write the HTML to a temp file for serving
141+
const tempHtmlPath = path.join(BUILD_WITH_ROOT_DIR, "test-css-order.html");
142+
// Replace absolute URLs to point to our dev server
143+
const localHtml = html.replace(/https:\/\/example\.com\//g, "http://localhost:5177/");
144+
await fs.writeFile(tempHtmlPath, localHtml);
145+
146+
// Create a simple HTTP server to serve the HTML file
147+
const { createServer: createHttpServer } = await import("http");
148+
const { readFile: fsReadFile } = await import("fs/promises");
149+
150+
const server = createHttpServer((req, res) => {
151+
void (async () => {
152+
try {
153+
const content = await fsReadFile(tempHtmlPath);
154+
res.writeHead(200, {
155+
"Content-Type": "text/html",
156+
"Access-Control-Allow-Origin": "*",
157+
});
158+
res.end(content);
159+
} catch {
160+
res.writeHead(404);
161+
res.end("Not found");
162+
}
163+
})();
164+
});
165+
166+
const port = 5178;
167+
await new Promise<void>((resolve) => {
168+
server.listen(port, resolve);
169+
});
170+
171+
try {
172+
const { chromium } = await import("playwright");
173+
const browser = await chromium.launch();
174+
const page = await browser.newPage();
175+
176+
await page.goto(`http://localhost:${port}/`, { waitUntil: "networkidle" });
177+
// Wait for styles to be injected
178+
await page.waitForTimeout(2000);
179+
180+
const html = await page.content();
181+
console.log(html);
182+
// Get all style tags from the document head, extracting their content
183+
const styleContents = await page.evaluate(() => {
184+
const styles = document.querySelectorAll("head style");
185+
return Array.from(styles).map((style) => style.textContent || "");
186+
});
187+
188+
await browser.close();
189+
190+
// Find which style tag contains root styles vs widget styles
191+
let rootStyleIndex = -1;
192+
let widgetStyleIndex = -1;
193+
194+
for (let i = 0; i < styleContents.length; i++) {
195+
const content = styleContents[i];
196+
if (content.includes(".root-layout") && rootStyleIndex === -1) {
197+
rootStyleIndex = i;
198+
}
199+
if (content.includes(".widget-a") && widgetStyleIndex === -1) {
200+
widgetStyleIndex = i;
201+
}
202+
}
203+
204+
// Verify both styles were found
205+
expect(rootStyleIndex).toBeGreaterThan(-1);
206+
expect(widgetStyleIndex).toBeGreaterThan(-1);
207+
208+
// Root layout styles should be injected BEFORE widget styles
209+
// This ensures proper CSS cascade - widget styles can override root styles
210+
expect(rootStyleIndex).toBeLessThan(widgetStyleIndex);
211+
} finally {
212+
server.close();
213+
await fs.unlink(tempHtmlPath).catch(() => {});
214+
}
215+
});
216+
});
117217
});

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,8 +437,8 @@ export function chatGPTWidgetPlugin(options: ChatGPTWidgetPluginOptions = {}): C
437437
return `
438438
${hmrRuntimeSetup}import React from 'react';
439439
import { createRoot } from 'react-dom/client';
440-
import Widget from '${widgetFile}';
441440
import RootLayout from '${rootFile}';
441+
import Widget from '${widgetFile}';
442442
443443
const container = document.getElementById('root');
444444
if (!container) {

0 commit comments

Comments
 (0)