Skip to content

Commit 3ff049a

Browse files
feat(preview): split base/user iframe styles and add @import regression coverage.
1 parent ed0e1f7 commit 3ff049a

3 files changed

Lines changed: 183 additions & 30 deletions

File tree

playwright/layout-panels.spec.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,24 +69,55 @@ test('changing preview background keeps applied preview styles', async ({ page }
6969

7070
await expect(previewFrameRoot).toHaveCount(1)
7171
const hasComponentStylesBefore = await previewFrameRoot.evaluate(() => {
72-
const styleElement = document.getElementById('knighted-preview-styles')
73-
if (!(styleElement instanceof HTMLStyleElement)) {
72+
const baseStyleElement = document.getElementById('knighted-preview-base-styles')
73+
const userStyleElement = document.getElementById('knighted-preview-user-styles')
74+
if (
75+
!(baseStyleElement instanceof HTMLStyleElement) ||
76+
!(userStyleElement instanceof HTMLStyleElement)
77+
) {
7478
return false
7579
}
7680

77-
return styleElement.textContent?.includes('.counter-button') ?? false
81+
const baseContainsReset =
82+
baseStyleElement.textContent?.includes('box-sizing: inherit;')
83+
const userContainsComponentStyles =
84+
userStyleElement.textContent?.includes('.counter-button')
85+
const styleElements = Array.from(document.head.querySelectorAll('style'))
86+
const baseIndex = styleElements.indexOf(baseStyleElement)
87+
const userIndex = styleElements.indexOf(userStyleElement)
88+
89+
return Boolean(
90+
baseContainsReset &&
91+
userContainsComponentStyles &&
92+
baseIndex >= 0 &&
93+
userIndex >= 0 &&
94+
baseIndex < userIndex,
95+
)
7896
})
7997
expect(hasComponentStylesBefore).toBe(true)
8098

8199
await page.getByLabel('Background').fill('#b1aaaa')
82100

83101
const hasComponentStylesAfter = await previewFrameRoot.evaluate(() => {
84-
const styleElement = document.getElementById('knighted-preview-styles')
85-
if (!(styleElement instanceof HTMLStyleElement)) {
102+
const baseStyleElements = document.querySelectorAll('#knighted-preview-base-styles')
103+
const userStyleElements = document.querySelectorAll('#knighted-preview-user-styles')
104+
if (baseStyleElements.length !== 1 || userStyleElements.length !== 1) {
86105
return false
87106
}
88107

89-
return styleElement.textContent?.includes('.counter-button') ?? false
108+
const baseStyleElement = baseStyleElements[0]
109+
const userStyleElement = userStyleElements[0]
110+
if (
111+
!(baseStyleElement instanceof HTMLStyleElement) ||
112+
!(userStyleElement instanceof HTMLStyleElement)
113+
) {
114+
return false
115+
}
116+
117+
return (
118+
(baseStyleElement.textContent?.includes('box-sizing: inherit;') ?? false) &&
119+
(userStyleElement.textContent?.includes('.counter-button') ?? false)
120+
)
90121
})
91122
expect(hasComponentStylesAfter).toBe(true)
92123

playwright/rendering-modes/core.spec.ts

Lines changed: 122 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,19 @@ const readLatestWorkspaceSnapshot = async (page: import('@playwright/test').Page
134134
})
135135
}
136136

137+
const readPreviewUserStyleText = async (page: import('@playwright/test').Page) => {
138+
return getPreviewFrame(page)
139+
.locator('html')
140+
.evaluate(() => {
141+
const userStyleElement = document.getElementById('knighted-preview-user-styles')
142+
if (!(userStyleElement instanceof HTMLStyleElement)) {
143+
return ''
144+
}
145+
146+
return userStyleElement.textContent ?? ''
147+
})
148+
}
149+
137150
test.beforeEach(async ({ page }) => {
138151
await resetWorkbenchStorage(page)
139152
})
@@ -283,11 +296,7 @@ test('preview styles require explicit import from entry graph', async ({ page })
283296

284297
await expect
285298
.poll(async () => {
286-
const styleContent = await getPreviewFrame(page)
287-
.locator('style')
288-
.first()
289-
.textContent()
290-
return styleContent ?? ''
299+
return readPreviewUserStyleText(page)
291300
})
292301
.toContain('rgb(1, 2, 3)')
293302

@@ -302,15 +311,117 @@ test('preview styles require explicit import from entry graph', async ({ page })
302311
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
303312
await expect
304313
.poll(async () => {
305-
const styleContent = await getPreviewFrame(page)
306-
.locator('style')
307-
.first()
308-
.textContent()
309-
return styleContent ?? ''
314+
return readPreviewUserStyleText(page)
310315
})
311316
.not.toContain('rgb(1, 2, 3)')
312317
})
313318

319+
test('top-level @import in user css is applied in preview iframe', async ({ page }) => {
320+
await waitForInitialRender(page)
321+
322+
const importedCss = encodeURIComponent('.counter-button { color: rgb(11, 22, 33); }')
323+
324+
await setWorkspaceTabSource(page, {
325+
fileName: 'app.css',
326+
kind: 'styles',
327+
source: [
328+
`@import url("data:text/css,${importedCss}");`,
329+
'.counter-button { font-weight: 700; }',
330+
].join('\n'),
331+
})
332+
333+
await setComponentEditorSource(
334+
page,
335+
[
336+
"import '../styles/app.css'",
337+
'',
338+
'const App = () => <button class="counter-button">Imported style</button>',
339+
'',
340+
].join('\n'),
341+
)
342+
343+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
344+
345+
await expect
346+
.poll(async () => {
347+
return getPreviewFrame(page)
348+
.getByRole('button', { name: 'Imported style' })
349+
.evaluate(element => getComputedStyle(element).color)
350+
})
351+
.toBe('rgb(11, 22, 33)')
352+
})
353+
354+
test('preview iframe keeps one base and one user style node across rerenders', async ({
355+
page,
356+
}) => {
357+
await waitForInitialRender(page)
358+
359+
await setWorkspaceTabSource(page, {
360+
fileName: 'app.css',
361+
kind: 'styles',
362+
source: ['.counter-button { color: rgb(40, 50, 60); }'].join('\n'),
363+
})
364+
365+
await page.getByLabel('Background').fill('#123456')
366+
367+
await setWorkspaceTabSource(page, {
368+
fileName: 'app.css',
369+
kind: 'styles',
370+
source: ['.counter-button { color: rgb(70, 80, 90); }'].join('\n'),
371+
})
372+
373+
await expect
374+
.poll(async () => {
375+
return getPreviewFrame(page)
376+
.locator('html')
377+
.evaluate(() => {
378+
const baseStyleElements = document.querySelectorAll(
379+
'#knighted-preview-base-styles',
380+
)
381+
const userStyleElements = document.querySelectorAll(
382+
'#knighted-preview-user-styles',
383+
)
384+
385+
const baseStyleElement = document.getElementById('knighted-preview-base-styles')
386+
const userStyleElement = document.getElementById('knighted-preview-user-styles')
387+
388+
if (
389+
!(baseStyleElement instanceof HTMLStyleElement) ||
390+
!(userStyleElement instanceof HTMLStyleElement)
391+
) {
392+
return {
393+
baseCount: baseStyleElements.length,
394+
userCount: userStyleElements.length,
395+
ordered: false,
396+
userText: '',
397+
}
398+
}
399+
400+
const styleElements = Array.from(document.head.querySelectorAll('style'))
401+
const baseIndex = styleElements.indexOf(baseStyleElement)
402+
const userIndex = styleElements.indexOf(userStyleElement)
403+
404+
return {
405+
baseCount: baseStyleElements.length,
406+
userCount: userStyleElements.length,
407+
ordered: baseIndex >= 0 && userIndex >= 0 && baseIndex < userIndex,
408+
userText: userStyleElement.textContent ?? '',
409+
}
410+
})
411+
})
412+
.toMatchObject({
413+
baseCount: 1,
414+
userCount: 1,
415+
ordered: true,
416+
})
417+
418+
await expect
419+
.poll(async () => {
420+
return readPreviewUserStyleText(page)
421+
})
422+
.toContain('rgb(70, 80, 90)')
423+
})
424+
314425
test('nested module imports can bring styles into preview graph', async ({ page }) => {
315426
await waitForInitialRender(page)
316427

@@ -347,11 +458,7 @@ test('nested module imports can bring styles into preview graph', async ({ page
347458
await expect(getPreviewFrame(page).getByRole('button')).toContainText('Nested style')
348459
await expect
349460
.poll(async () => {
350-
const styleContent = await getPreviewFrame(page)
351-
.locator('style')
352-
.first()
353-
.textContent()
354-
return styleContent ?? ''
461+
return readPreviewUserStyleText(page)
355462
})
356463
.toContain('rgb(9, 8, 7)')
357464
})

src/modules/preview-runtime/iframe-preview-executor.js

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,32 @@ const createIframeShellDocument = ({ channelId, parentOrigin, importMap }) => {
121121
backgroundColor: typeof backgroundColor === 'string' ? backgroundColor : '',
122122
}
123123
124-
let styleElement = document.getElementById('knighted-preview-styles')
125-
if (!(styleElement instanceof HTMLStyleElement)) {
126-
styleElement = document.createElement('style')
127-
styleElement.id = 'knighted-preview-styles'
128-
document.head.append(styleElement)
124+
let baseStyleElement = document.getElementById('knighted-preview-base-styles')
125+
if (!(baseStyleElement instanceof HTMLStyleElement)) {
126+
baseStyleElement = document.createElement('style')
127+
baseStyleElement.id = 'knighted-preview-base-styles'
128+
document.head.append(baseStyleElement)
129129
}
130130
131-
styleElement.textContent =
132-
__knightedToBaseStyles(__knightedState.visualConfig.hostPadding) +
133-
'\\n' +
134-
String(__knightedState.visualConfig.cssText)
131+
let userStyleElement = document.getElementById('knighted-preview-user-styles')
132+
if (!(userStyleElement instanceof HTMLStyleElement)) {
133+
userStyleElement = document.createElement('style')
134+
userStyleElement.id = 'knighted-preview-user-styles'
135+
document.head.append(userStyleElement)
136+
}
137+
138+
const isBaseAfterUser =
139+
(baseStyleElement.compareDocumentPosition(userStyleElement) &
140+
Node.DOCUMENT_POSITION_PRECEDING) !==
141+
0
142+
if (isBaseAfterUser) {
143+
document.head.insertBefore(baseStyleElement, userStyleElement)
144+
}
145+
146+
baseStyleElement.textContent = __knightedToBaseStyles(
147+
__knightedState.visualConfig.hostPadding,
148+
)
149+
userStyleElement.textContent = String(__knightedState.visualConfig.cssText)
135150
136151
if (__knightedState.visualConfig.hostPadding.trim().length > 0) {
137152
document.documentElement.style.setProperty(

0 commit comments

Comments
 (0)