Skip to content

Commit 04bef8f

Browse files
feat: require explicit css imports, render mode entry only.
1 parent fd9a617 commit 04bef8f

8 files changed

Lines changed: 709 additions & 88 deletions

File tree

playwright/diagnostics.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,23 @@ test('styles diagnostics rows navigate editor to reported line', async ({ page }
255255
await expect.poll(() => getActiveStylesEditorLineNumber(page)).toBe('3')
256256
})
257257

258+
test('sass compiler warnings surface in styles diagnostics', async ({ page }) => {
259+
await waitForInitialRender(page)
260+
261+
await ensurePanelToolsVisible(page, 'styles')
262+
await page.getByRole('combobox', { name: 'Style mode' }).selectOption('sass')
263+
await setStylesEditorSource(
264+
page,
265+
['.card {', ' color: darken(#ff0000, 10%);', '}'].join('\n'),
266+
)
267+
268+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
269+
await ensureDiagnosticsDrawerOpen(page)
270+
await expect(page.locator('#diagnostics-styles')).toContainText(
271+
'Style compilation warnings.',
272+
)
273+
})
274+
258275
test('clear component diagnostics resets rendered lint-issue status pill', async ({
259276
page,
260277
}) => {

playwright/rendering-modes.spec.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,90 @@ test('renders in react mode with css modules', async ({ page }) => {
4747
await expectPreviewHasRenderedContent(page)
4848
})
4949

50+
test('preview styles require explicit import from entry graph', async ({ page }) => {
51+
await waitForInitialRender(page)
52+
53+
await setWorkspaceTabSource(page, {
54+
fileName: 'app.css',
55+
kind: 'styles',
56+
source: ['.counter-button { color: rgb(1, 2, 3); }'].join('\n'),
57+
})
58+
59+
await expect
60+
.poll(async () => {
61+
const styleContent = await getPreviewFrame(page)
62+
.locator('style')
63+
.first()
64+
.textContent()
65+
return styleContent ?? ''
66+
})
67+
.toContain('rgb(1, 2, 3)')
68+
69+
await setComponentEditorSource(
70+
page,
71+
[
72+
'const App = () => <button class="counter-button">No style import</button>',
73+
'',
74+
].join('\n'),
75+
)
76+
77+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
78+
await expect
79+
.poll(async () => {
80+
const styleContent = await getPreviewFrame(page)
81+
.locator('style')
82+
.first()
83+
.textContent()
84+
return styleContent ?? ''
85+
})
86+
.not.toContain('rgb(1, 2, 3)')
87+
})
88+
89+
test('nested module imports can bring styles into preview graph', async ({ page }) => {
90+
await waitForInitialRender(page)
91+
92+
await addWorkspaceTab(page, { kind: 'component' })
93+
await addWorkspaceTab(page, { kind: 'styles' })
94+
95+
await setWorkspaceTabSource(page, {
96+
fileName: 'module.tsx',
97+
kind: 'component',
98+
source: [
99+
"import '../styles/module.css'",
100+
'',
101+
'export const ModuleButton = () => <button class="module-button">Nested style</button>',
102+
].join('\n'),
103+
})
104+
105+
await setWorkspaceTabSource(page, {
106+
fileName: 'module.css',
107+
kind: 'styles',
108+
source: ['.module-button { color: rgb(9, 8, 7); }'].join('\n'),
109+
})
110+
111+
await setComponentEditorSource(
112+
page,
113+
[
114+
"import { ModuleButton } from './module'",
115+
'',
116+
'const App = () => <ModuleButton />',
117+
'',
118+
].join('\n'),
119+
)
120+
121+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
122+
await expect(getPreviewFrame(page).getByRole('button')).toContainText('Nested style')
123+
await expect
124+
.poll(async () => {
125+
const styleContent = await getPreviewFrame(page)
126+
.locator('style')
127+
.first()
128+
.textContent()
129+
return styleContent ?? ''
130+
})
131+
.toContain('rgb(9, 8, 7)')
132+
})
133+
50134
test('transpiles TypeScript annotations in component source', async ({ page }) => {
51135
await waitForInitialRender(page)
52136

@@ -641,6 +725,20 @@ test('persists render mode across reload', async ({ page }) => {
641725
await expect(page.getByRole('combobox', { name: 'Render mode' })).toHaveValue('react')
642726
})
643727

728+
test('persists style mode across reload', async ({ page }) => {
729+
await waitForInitialRender(page)
730+
731+
await ensurePanelToolsVisible(page, 'styles')
732+
await page.getByRole('combobox', { name: 'Style mode' }).selectOption('sass')
733+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
734+
735+
await page.reload()
736+
await waitForInitialRender(page)
737+
await ensurePanelToolsVisible(page, 'styles')
738+
739+
await expect(page.getByRole('combobox', { name: 'Style mode' })).toHaveValue('sass')
740+
})
741+
644742
test('renders with less style mode', async ({ page }) => {
645743
await waitForInitialRender(page)
646744

@@ -827,6 +925,97 @@ test('workspace graph errors for missing modules remain deterministic', async ({
827925
)
828926
})
829927

928+
test('renaming an imported module tab re-renders and surfaces missing import errors', async ({
929+
page,
930+
}) => {
931+
await waitForInitialRender(page)
932+
933+
await ensurePanelToolsVisible(page, 'component')
934+
935+
await addWorkspaceTab(page)
936+
await setWorkspaceTabSource(page, {
937+
fileName: 'module.tsx',
938+
source: [
939+
'export const ItemWrap = ({ children }: { children: string }) => {',
940+
' return <span>{children}</span>',
941+
'}',
942+
].join('\n'),
943+
})
944+
945+
await setWorkspaceTabSource(page, {
946+
fileName: 'App.tsx',
947+
source: [
948+
"import { ItemWrap } from './module'",
949+
'export const App = () => <ItemWrap>hello</ItemWrap>',
950+
].join('\n'),
951+
})
952+
953+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
954+
955+
await page.getByRole('button', { name: 'Rename tab module.tsx' }).click()
956+
const renameInput = page.getByLabel('Rename module.tsx')
957+
await renameInput.fill('module-renamed.tsx')
958+
await renameInput.press('Enter')
959+
960+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error')
961+
await expect(page.locator('#preview-host pre')).toContainText(
962+
'Preview entry references missing workspace module: ./module',
963+
)
964+
})
965+
966+
test('renaming default styles tab updates graph resolution and surfaces stale import', async ({
967+
page,
968+
}) => {
969+
await waitForInitialRender(page)
970+
971+
await ensurePanelToolsVisible(page, 'styles')
972+
973+
await page.getByRole('button', { name: 'Rename tab app.css' }).click()
974+
const renameInput = page.getByLabel('Rename app.css')
975+
await renameInput.fill('app.less')
976+
await renameInput.press('Enter')
977+
978+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Error')
979+
await expect(page.locator('#preview-host pre')).toContainText(
980+
'Preview entry references missing workspace module: ../styles/app.css',
981+
)
982+
983+
await setWorkspaceTabSource(page, {
984+
fileName: 'App.tsx',
985+
source: [
986+
"import '../styles/app.less'",
987+
'',
988+
'type CounterButtonProps = {',
989+
' label: string',
990+
' onClick: (event: MouseEvent) => void',
991+
'}',
992+
'',
993+
'const CounterButton = ({ label, onClick }: CounterButtonProps) => (',
994+
' <button class="counter-button" type="button" onClick={onClick}>',
995+
' {label}',
996+
' </button>',
997+
')',
998+
'',
999+
'const App = () => {',
1000+
' let count = 0',
1001+
' const handleClick = (event: MouseEvent) => {',
1002+
' count += 1',
1003+
' const button = event.currentTarget as HTMLButtonElement',
1004+
' button.textContent = `Clicks: ${count}`',
1005+
" button.dataset.active = count % 2 === 0 ? 'false' : 'true'",
1006+
" button.classList.toggle('is-even', count % 2 === 0)",
1007+
' }',
1008+
'',
1009+
" return <CounterButton label='Clicks: 0' onClick={handleClick} />",
1010+
'}',
1011+
'',
1012+
].join('\n'),
1013+
})
1014+
1015+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
1016+
await expect(page.locator('#preview-host pre')).toHaveCount(0)
1017+
})
1018+
8301019
test('workspace graph errors for circular imports remain deterministic', async ({
8311020
page,
8321021
}) => {
@@ -902,6 +1091,8 @@ test('children runtime errors recover after module fix and mode switches', async
9021091
await expect(page.locator('#preview-host pre')).toHaveCount(0)
9031092
await expect(getPreviewFrame(page).getByText('hello children')).toBeVisible()
9041093

1094+
await openWorkspaceTab(page, 'App.tsx')
1095+
await ensurePanelToolsVisible(page, 'component')
9051096
await page.getByRole('combobox', { name: 'Render mode' }).selectOption('react')
9061097
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
9071098
await expect(page.locator('#preview-host pre')).toHaveCount(0)

playwright/workspace-tabs.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { expect, test } from '@playwright/test'
22
import {
33
addWorkspaceTab,
4+
ensurePanelToolsVisible,
45
reorderWorkspaceTabBefore,
56
setWorkspaceTabSource,
67
waitForInitialRender,
@@ -148,6 +149,28 @@ test('active tab remains source of truth for visible editor panel', async ({ pag
148149
await expect(componentPanel).toHaveAttribute('hidden', '')
149150
})
150151

152+
test('render mode can only be changed from entry tab', async ({ page }) => {
153+
await waitForInitialRender(page)
154+
155+
await addWorkspaceTab(page)
156+
await ensurePanelToolsVisible(page, 'component')
157+
158+
const renderMode = page.getByRole('combobox', { name: 'Render mode' })
159+
160+
await page.getByRole('button', { name: 'Open tab App.tsx' }).click()
161+
await expect(renderMode).toBeEnabled()
162+
await renderMode.selectOption('react')
163+
await expect(renderMode).toHaveValue('react')
164+
165+
await page.getByRole('button', { name: 'Open tab module.tsx' }).click()
166+
await expect(renderMode).toBeDisabled()
167+
await expect(renderMode).toHaveValue('react')
168+
169+
await page.getByRole('button', { name: 'Open tab App.tsx' }).click()
170+
await expect(renderMode).toBeEnabled()
171+
await expect(renderMode).toHaveValue('react')
172+
})
173+
151174
test('startup restores last active workspace tab after reload', async ({ page }) => {
152175
await waitForInitialRender(page)
153176

0 commit comments

Comments
 (0)