Skip to content

Commit 2258ab8

Browse files
fix(diagnostics): enforce current-editor snapshots and stale results. (#95)
1 parent 729556d commit 2258ab8

10 files changed

Lines changed: 598 additions & 73 deletions

playwright/diagnostics.spec.ts

Lines changed: 146 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ test('clear component diagnostics removes type errors and restores rendered stat
6262
await expect(page.getByText(/Rendered \(Type errors: [1-9]\d*\)/)).toBeVisible()
6363

6464
await ensureDiagnosticsDrawerOpen(page)
65-
await page.getByRole('button', { name: 'Reset component' }).click()
65+
await page.getByRole('button', { name: 'Reset types' }).click()
6666

6767
await expect(page.getByText('No diagnostics yet.')).toHaveCount(2)
6868
await expect(diagnosticsToggle).toHaveText('Diagnostics')
@@ -419,9 +419,9 @@ test('clear component diagnostics resets rendered lint-issue status pill', async
419419
)
420420

421421
await ensureDiagnosticsDrawerOpen(page)
422-
await page.getByRole('button', { name: 'Reset component' }).click()
422+
await page.getByRole('button', { name: 'Reset lint' }).click()
423423

424-
await expect(page.getByText('No diagnostics yet.')).toHaveCount(2)
424+
await expect(page.locator('#diagnostics-styles')).toContainText('No diagnostics yet.')
425425
await expect(diagnosticsToggle).toHaveText('Diagnostics')
426426
await expect(diagnosticsToggle).toHaveClass(/diagnostics-toggle--neutral/)
427427
await expect(page.getByText('Rendered', { exact: true })).toHaveClass(/status--neutral/)
@@ -458,7 +458,7 @@ test('component lint ignores only unused App binding', async ({ page }) => {
458458
expect(diagnosticsText).toContain('This function render is unused')
459459
})
460460

461-
test('component lint with unresolved issues enters pending diagnostics state while typing', async ({
461+
test('component lint with unresolved issues becomes stale and waits for manual rerun', async ({
462462
page,
463463
}) => {
464464
await waitForInitialRender(page)
@@ -481,10 +481,147 @@ test('component lint with unresolved issues enters pending diagnostics state whi
481481
),
482482
)
483483

484-
await expect(diagnosticsToggle).toHaveClass(/diagnostics-toggle--pending/)
485-
await expect(diagnosticsToggle).toHaveAttribute('aria-busy', 'true')
486-
487-
await expect(page.getByText(/Rendered \(Lint issues: [1-9]\d*\)/)).toBeVisible()
488-
await expect(diagnosticsToggle).toHaveClass(/diagnostics-toggle--error/)
484+
await expect(diagnosticsToggle).toHaveClass(/diagnostics-toggle--neutral/)
489485
await expect(diagnosticsToggle).toHaveAttribute('aria-busy', 'false')
486+
487+
await ensureDiagnosticsDrawerOpen(page)
488+
await expect(page.locator('#diagnostics-styles')).toContainText(
489+
'Source changed. Click Lint to run diagnostics.',
490+
)
491+
492+
await expect(page.getByText('Rendered', { exact: true })).toBeVisible()
493+
})
494+
495+
test('styles active tab shows lint-only diagnostics drawer actions', async ({ page }) => {
496+
await waitForInitialRender(page)
497+
498+
await ensurePanelToolsVisible(page, 'styles')
499+
await ensureDiagnosticsDrawerOpen(page)
500+
501+
await expect(page.locator('[data-diagnostics-scope="component"]')).toBeHidden()
502+
await expect(page.locator('#diagnostics-clear-styles')).toHaveText('Reset lint')
503+
await expect(page.locator('#diagnostics-clear-styles')).toBeVisible()
504+
await expect(page.locator('#diagnostics-clear-component')).toBeHidden()
505+
await expect(page.locator('#diagnostics-clear-all')).toBeHidden()
506+
})
507+
508+
test('component lint completion is ignored after switching to another component tab', async ({
509+
page,
510+
}) => {
511+
await waitForInitialRender(page)
512+
513+
await ensurePanelToolsVisible(page, 'component')
514+
await addWorkspaceTab(page)
515+
516+
const heavyLintSource = [
517+
...Array.from({ length: 120 }, (_, index) => `const unused${index} = ${index}`),
518+
'const App = () => <button>module tab</button>',
519+
].join('\n')
520+
521+
await setWorkspaceTabSource(page, {
522+
fileName: 'module.tsx',
523+
kind: 'component',
524+
source: heavyLintSource,
525+
})
526+
527+
const lintTrigger = page.getByRole('button', { name: 'Lint' }).first()
528+
await lintTrigger.click()
529+
530+
await setComponentEditorSource(
531+
page,
532+
'const App = () => <button type="button">A</button>',
533+
)
534+
535+
await expect(page.locator('#diagnostics-styles')).toContainText(
536+
'Source changed. Click Lint to run diagnostics.',
537+
)
538+
await expect(page.locator('#diagnostics-styles')).not.toContainText(
539+
'Biome reported issues.',
540+
)
541+
await expect(page.getByRole('button', { name: /^Diagnostics/ })).toHaveClass(
542+
/diagnostics-toggle--neutral/,
543+
)
544+
})
545+
546+
test('switching tabs clears diagnostics while drawer is open', async ({ page }) => {
547+
await waitForInitialRender(page)
548+
549+
await setComponentEditorSource(page, 'const App = () => <button>lint me</button>')
550+
await runComponentLint(page)
551+
552+
await ensureDiagnosticsDrawerOpen(page)
553+
await expect(page.locator('#diagnostics-styles')).toContainText(
554+
'Biome reported issues.',
555+
)
556+
557+
await page.getByRole('button', { name: 'Open tab app.css' }).click()
558+
559+
await expect(page.locator('#diagnostics-styles')).toContainText('No diagnostics yet.')
560+
await expect(page.getByRole('button', { name: /^Diagnostics/ })).toHaveClass(
561+
/diagnostics-toggle--neutral/,
562+
)
563+
})
564+
565+
test('same-tab edits with drawer open replace lint issues with stale state', async ({
566+
page,
567+
}) => {
568+
await waitForInitialRender(page)
569+
570+
await setComponentEditorSource(
571+
page,
572+
['const count: string = 1', 'const App = () => <button>Inactive</button>'].join('\n'),
573+
)
574+
575+
await runTypecheck(page)
576+
await runComponentLint(page)
577+
await ensureDiagnosticsDrawerOpen(page)
578+
579+
await expect(page.locator('#diagnostics-component')).toContainText('TypeScript found')
580+
await expect(page.locator('#diagnostics-styles')).toContainText(
581+
'Biome reported issues.',
582+
)
583+
584+
await setComponentEditorSource(
585+
page,
586+
[
587+
'const count: string = "ok"',
588+
'const App = () => <button type="button">Inactive</button>',
589+
].join('\n'),
590+
)
591+
592+
await expect(page.locator('#diagnostics-component')).not.toContainText('TS2322')
593+
await expect(page.locator('#diagnostics-styles')).toContainText(
594+
'Source changed. Click Lint to run diagnostics.',
595+
)
596+
await expect(page.locator('#diagnostics-styles')).not.toContainText(
597+
'Biome reported issues.',
598+
)
599+
})
600+
601+
test('reset lint on styles tab clears in-flight component lint state', async ({
602+
page,
603+
}) => {
604+
await waitForInitialRender(page)
605+
606+
const heavyLintSource = [
607+
...Array.from({ length: 120 }, (_, index) => `const unused${index} = ${index}`),
608+
'const App = () => <button>component tab</button>',
609+
].join('\n')
610+
611+
await setComponentEditorSource(page, heavyLintSource)
612+
613+
await ensureDiagnosticsDrawerOpen(page)
614+
await page.getByRole('button', { name: 'Lint' }).first().click()
615+
616+
await page.getByRole('button', { name: 'Open tab app.css' }).click()
617+
await page.locator('#diagnostics-clear-styles').click()
618+
619+
await expect(page.getByRole('button', { name: /^Diagnostics/ })).toHaveAttribute(
620+
'aria-busy',
621+
'false',
622+
)
623+
await expect(page.locator('#diagnostics-styles')).toContainText('No diagnostics yet.')
624+
await expect(page.locator('#diagnostics-styles')).not.toContainText(
625+
'Biome reported issues.',
626+
)
490627
})

src/app.js

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
createWorkspaceContextSnapshotGetter,
2929
toStyleModeForTabLanguage,
3030
} from './modules/app-core/workspace-local-helpers.js'
31+
import { createDiagnosticsTabStateHelpers } from './modules/app-core/diagnostics-tab-state-helpers.js'
3132
import { createWorkspaceEditorHelpers } from './modules/app-core/workspace-editor-helpers.js'
3233
import { createEditedIndicatorVisibilityController } from './modules/app-core/edited-indicator-visibility-controller.js'
3334
import { createPublishTrailingNewlineNormalizer } from './modules/app-core/publish-trailing-newline-normalizer.js'
@@ -187,6 +188,14 @@ const diagnosticsClearStyles = document.getElementById('diagnostics-clear-styles
187188
const diagnosticsClearAll = document.getElementById('diagnostics-clear-all')
188189
const diagnosticsComponent = document.getElementById('diagnostics-component')
189190
const diagnosticsStyles = document.getElementById('diagnostics-styles')
191+
const diagnosticsComponentSection = document.querySelector(
192+
'[data-diagnostics-scope="component"]',
193+
)
194+
const diagnosticsStylesSection = document.querySelector(
195+
'[data-diagnostics-scope="styles"]',
196+
)
197+
const diagnosticsComponentHeading = diagnosticsComponentSection?.querySelector('h3')
198+
const diagnosticsStylesHeading = diagnosticsStylesSection?.querySelector('h3')
190199
const appToast = document.getElementById('app-toast')
191200
const previewBgColorInput = document.getElementById('preview-bg-color')
192201
const clearConfirmDialog = document.getElementById('clear-confirm-dialog')
@@ -224,6 +233,7 @@ cssEditor.value = defaultCss
224233
let previewHost = document.getElementById('preview-host')
225234
let jsxCodeEditor = null
226235
let cssCodeEditor = null
236+
let diagnosticsFlowController = null
227237
let getJsxSource = () => jsxEditor.value
228238
let getCssSource = () => cssEditor.value
229239
let renderRuntime = null
@@ -647,11 +657,40 @@ const workspaceSyncController = createWorkspaceSyncController({
647657
const getLoadedComponentWorkspaceTab = () =>
648658
workspaceTabsState.getTab(loadedComponentTabId) ?? getWorkspaceTabByKind('component')
649659

660+
const getLoadedStylesWorkspaceTab = () =>
661+
workspaceTabsState.getTab(loadedStylesTabId) ?? getWorkspaceTabByKind('styles')
662+
650663
const getTypecheckSourcePath = () => {
651664
const loadedComponentTab = getLoadedComponentWorkspaceTab()
652665
return toNonEmptyWorkspaceText(loadedComponentTab?.path) || defaultComponentTabPath
653666
}
654667

668+
const {
669+
clearDiagnosticsOnTabSwitch,
670+
getComponentLintTarget,
671+
getStylesLintTarget,
672+
syncDiagnosticsDrawerLayout,
673+
} = createDiagnosticsTabStateHelpers({
674+
getActiveWorkspaceTab,
675+
getLoadedComponentWorkspaceTab,
676+
getLoadedStylesWorkspaceTab,
677+
getTabKind,
678+
toNonEmptyWorkspaceText,
679+
diagnosticsComponentSection,
680+
diagnosticsStylesSection,
681+
diagnosticsComponentHeading,
682+
diagnosticsStylesHeading,
683+
diagnosticsClearComponent,
684+
diagnosticsClearStyles,
685+
diagnosticsClearAll,
686+
clearAllDiagnostics,
687+
setTypeDiagnosticsPending,
688+
setLintDiagnosticsPending,
689+
statusNode,
690+
setStatus,
691+
getDiagnosticsFlowController: () => diagnosticsFlowController,
692+
})
693+
655694
const createWorkspaceTabId = prefix => createWorkspaceTabIdFactory(prefix)
656695

657696
const makeUniqueTabPath = ({ basePath, suffix = '' }) =>
@@ -734,6 +773,13 @@ const {
734773
},
735774
persistRenderMode: mode => persistRenderMode(mode),
736775
getActiveWorkspaceTab,
776+
onActiveWorkspaceTabChange: (_tab, { changed } = {}) => {
777+
syncDiagnosticsDrawerLayout()
778+
779+
if (changed) {
780+
clearDiagnosticsOnTabSwitch()
781+
}
782+
},
737783
loadWorkspaceTabIntoEditor,
738784
updateRenderModeEditability: () => updateRenderModeEditability(),
739785
getHasCompletedInitialWorkspaceBootstrap: () => hasCompletedInitialWorkspaceBootstrap,
@@ -1223,6 +1269,8 @@ const runtimeCoreOptions = createRuntimeCoreOptions({
12231269
getJsxSource: () => getJsxSource(),
12241270
getCssSource: () => getCssSource(),
12251271
getTypecheckSourcePath,
1272+
getComponentLintTarget,
1273+
getStylesLintTarget,
12261274
buildWorkspaceTabsSnapshot,
12271275
renderMode,
12281276
styleMode,
@@ -1267,7 +1315,7 @@ const runtimeCoreOptions = createRuntimeCoreOptions({
12671315
})
12681316
const runtimeCore = createRuntimeCoreSetup(runtimeCoreOptions)
12691317

1270-
const diagnosticsFlowController = runtimeCore.diagnosticsFlowController
1318+
diagnosticsFlowController = runtimeCore.diagnosticsFlowController
12711319
renderRuntime = runtimeCore.renderRuntime
12721320
const setCdnLoading = runtimeCore.setCdnLoading
12731321
const typeDiagnostics = diagnosticsFlowController.typeDiagnostics
@@ -1360,6 +1408,8 @@ bindAppEventsAndStart({
13601408
renderPreview,
13611409
setJsxSource,
13621410
setCssSource,
1411+
persistActiveTabEditorContent,
1412+
getWorkspaceTabsSnapshot: () => workspaceTabsState.getTabs(),
13631413
queueWorkspaceSave,
13641414
maybeRender,
13651415
maybeRenderFromComponentEditorChange,
@@ -1389,6 +1439,7 @@ bindAppEventsAndStart({
13891439
updateRenderModeEditability,
13901440
loadPreferredWorkspaceContext,
13911441
getActiveWorkspaceTab,
1442+
getTabKind,
13921443
setActiveWorkspaceTab,
13931444
workspaceTabsState,
13941445
loadedStylesTabIdRef: {
@@ -1397,6 +1448,7 @@ bindAppEventsAndStart({
13971448
},
13981449
},
13991450
getWorkspaceTabByKind,
1451+
syncDiagnosticsDrawerLayout,
14001452
workspaceSaveController,
14011453
workspaceStorage,
14021454
bindWorkspaceMetadataPersistence,

0 commit comments

Comments
 (0)