Skip to content

Commit 4587cba

Browse files
feat: explicit css imports, diagnostics per editor buffer. (#71)
1 parent fd9a617 commit 4587cba

10 files changed

Lines changed: 1124 additions & 153 deletions

File tree

playwright/diagnostics.spec.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { expect, test } from '@playwright/test'
22
import {
3+
addWorkspaceTab,
34
ensurePanelToolsVisible,
45
ensureDiagnosticsDrawerOpen,
56
getActiveComponentEditorLineNumber,
67
getActiveStylesEditorLineNumber,
8+
setWorkspaceTabSource,
79
runComponentLint,
810
runStylesLint,
911
runTypecheck,
@@ -147,6 +149,99 @@ test('dom mode typecheck resolves @knighted/jsx type-only imports', async ({ pag
147149
expect(diagnosticsText).not.toContain("Cannot find module '@knighted/jsx'")
148150
})
149151

152+
test('typecheck resolves .js import to workspace tsx module tab', async ({ page }) => {
153+
await waitForInitialRender(page)
154+
155+
await ensurePanelToolsVisible(page, 'component')
156+
await addWorkspaceTab(page)
157+
158+
await setWorkspaceTabSource(page, {
159+
fileName: 'module.tsx',
160+
kind: 'component',
161+
source: [
162+
'type ThingProps = { label: string }',
163+
'export const Thing = ({ label }: ThingProps) => <p>{label}</p>',
164+
].join('\n'),
165+
})
166+
167+
await setComponentEditorSource(
168+
page,
169+
[
170+
"import { Thing } from './module.js'",
171+
'const App = () => <Thing label="ok" />',
172+
'',
173+
].join('\n'),
174+
)
175+
176+
await runTypecheck(page)
177+
await ensureDiagnosticsDrawerOpen(page)
178+
await expect(page.locator('#diagnostics-component')).toContainText(
179+
'No TypeScript errors found.',
180+
)
181+
182+
const diagnosticsText = await page.locator('#diagnostics-component').innerText()
183+
expect(diagnosticsText).not.toContain("Cannot find module './module.js'")
184+
})
185+
186+
test('typecheck resolves parent-relative .js import to workspace tsx module tab', async ({
187+
page,
188+
}) => {
189+
await waitForInitialRender(page)
190+
191+
await ensurePanelToolsVisible(page, 'component')
192+
await addWorkspaceTab(page)
193+
194+
await setWorkspaceTabSource(page, {
195+
fileName: 'module.tsx',
196+
kind: 'component',
197+
source: [
198+
'type ThingProps = { label: string }',
199+
'export const Thing = ({ label }: ThingProps) => <p>{label}</p>',
200+
].join('\n'),
201+
})
202+
203+
await setComponentEditorSource(
204+
page,
205+
[
206+
"import { Thing } from '../components/module.js'",
207+
'const App = () => <Thing label="ok" />',
208+
'',
209+
].join('\n'),
210+
)
211+
212+
await runTypecheck(page)
213+
await ensureDiagnosticsDrawerOpen(page)
214+
await expect(page.locator('#diagnostics-component')).toContainText(
215+
'No TypeScript errors found.',
216+
)
217+
218+
const diagnosticsText = await page.locator('#diagnostics-component').innerText()
219+
expect(diagnosticsText).not.toContain("Cannot find module '../components/module.js'")
220+
})
221+
222+
test('typecheck does not report TS2307 for stylesheet side-effect imports', async ({
223+
page,
224+
}) => {
225+
await waitForInitialRender(page)
226+
227+
await ensurePanelToolsVisible(page, 'component')
228+
await setComponentEditorSource(
229+
page,
230+
["import '../styles/app.css'", '', 'const App = () => <p>style import</p>', ''].join(
231+
'\n',
232+
),
233+
)
234+
235+
await runTypecheck(page)
236+
await ensureDiagnosticsDrawerOpen(page)
237+
await expect(page.locator('#diagnostics-component')).toContainText(
238+
'No TypeScript errors found.',
239+
)
240+
241+
const diagnosticsText = await page.locator('#diagnostics-component').innerText()
242+
expect(diagnosticsText).not.toContain("Cannot find module '../styles/app.css'")
243+
})
244+
150245
test('component diagnostics rows navigate editor to reported line', async ({ page }) => {
151246
await waitForInitialRender(page)
152247

@@ -255,6 +350,23 @@ test('styles diagnostics rows navigate editor to reported line', async ({ page }
255350
await expect.poll(() => getActiveStylesEditorLineNumber(page)).toBe('3')
256351
})
257352

353+
test('sass compiler warnings surface in styles diagnostics', async ({ page }) => {
354+
await waitForInitialRender(page)
355+
356+
await ensurePanelToolsVisible(page, 'styles')
357+
await page.getByRole('combobox', { name: 'Style mode' }).selectOption('sass')
358+
await setStylesEditorSource(
359+
page,
360+
['.card {', ' color: darken(#ff0000, 10%);', '}'].join('\n'),
361+
)
362+
363+
await expect(page.getByRole('status', { name: 'App status' })).toHaveText('Rendered')
364+
await ensureDiagnosticsDrawerOpen(page)
365+
await expect(page.locator('#diagnostics-styles')).toContainText(
366+
'Style compilation warnings.',
367+
)
368+
})
369+
258370
test('clear component diagnostics resets rendered lint-issue status pill', async ({
259371
page,
260372
}) => {

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)

0 commit comments

Comments
 (0)