Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 146 additions & 9 deletions playwright/diagnostics.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ test('clear component diagnostics removes type errors and restores rendered stat
await expect(page.getByText(/Rendered \(Type errors: [1-9]\d*\)/)).toBeVisible()

await ensureDiagnosticsDrawerOpen(page)
await page.getByRole('button', { name: 'Reset component' }).click()
await page.getByRole('button', { name: 'Reset types' }).click()

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

await ensureDiagnosticsDrawerOpen(page)
await page.getByRole('button', { name: 'Reset component' }).click()
await page.getByRole('button', { name: 'Reset lint' }).click()

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

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

await expect(diagnosticsToggle).toHaveClass(/diagnostics-toggle--pending/)
await expect(diagnosticsToggle).toHaveAttribute('aria-busy', 'true')

await expect(page.getByText(/Rendered \(Lint issues: [1-9]\d*\)/)).toBeVisible()
await expect(diagnosticsToggle).toHaveClass(/diagnostics-toggle--error/)
await expect(diagnosticsToggle).toHaveClass(/diagnostics-toggle--neutral/)
await expect(diagnosticsToggle).toHaveAttribute('aria-busy', 'false')

await ensureDiagnosticsDrawerOpen(page)
await expect(page.locator('#diagnostics-styles')).toContainText(
'Source changed. Click Lint to run diagnostics.',
)

await expect(page.getByText('Rendered', { exact: true })).toBeVisible()
})

test('styles active tab shows lint-only diagnostics drawer actions', async ({ page }) => {
await waitForInitialRender(page)

await ensurePanelToolsVisible(page, 'styles')
await ensureDiagnosticsDrawerOpen(page)

await expect(page.locator('[data-diagnostics-scope="component"]')).toBeHidden()
await expect(page.locator('#diagnostics-clear-styles')).toHaveText('Reset lint')
await expect(page.locator('#diagnostics-clear-styles')).toBeVisible()
await expect(page.locator('#diagnostics-clear-component')).toBeHidden()
await expect(page.locator('#diagnostics-clear-all')).toBeHidden()
})

test('component lint completion is ignored after switching to another component tab', async ({
page,
}) => {
await waitForInitialRender(page)

await ensurePanelToolsVisible(page, 'component')
await addWorkspaceTab(page)

const heavyLintSource = [
...Array.from({ length: 120 }, (_, index) => `const unused${index} = ${index}`),
'const App = () => <button>module tab</button>',
].join('\n')

await setWorkspaceTabSource(page, {
fileName: 'module.tsx',
kind: 'component',
source: heavyLintSource,
})

const lintTrigger = page.getByRole('button', { name: 'Lint' }).first()
await lintTrigger.click()

await setComponentEditorSource(
page,
'const App = () => <button type="button">A</button>',
)

await expect(page.locator('#diagnostics-styles')).toContainText(
'Source changed. Click Lint to run diagnostics.',
)
await expect(page.locator('#diagnostics-styles')).not.toContainText(
'Biome reported issues.',
)
await expect(page.getByRole('button', { name: /^Diagnostics/ })).toHaveClass(
/diagnostics-toggle--neutral/,
)
})

test('switching tabs clears diagnostics while drawer is open', async ({ page }) => {
await waitForInitialRender(page)

await setComponentEditorSource(page, 'const App = () => <button>lint me</button>')
await runComponentLint(page)

await ensureDiagnosticsDrawerOpen(page)
await expect(page.locator('#diagnostics-styles')).toContainText(
'Biome reported issues.',
)

await page.getByRole('button', { name: 'Open tab app.css' }).click()

await expect(page.locator('#diagnostics-styles')).toContainText('No diagnostics yet.')
await expect(page.getByRole('button', { name: /^Diagnostics/ })).toHaveClass(
/diagnostics-toggle--neutral/,
)
})

test('same-tab edits with drawer open replace lint issues with stale state', async ({
page,
}) => {
await waitForInitialRender(page)

await setComponentEditorSource(
page,
['const count: string = 1', 'const App = () => <button>Inactive</button>'].join('\n'),
)

await runTypecheck(page)
await runComponentLint(page)
await ensureDiagnosticsDrawerOpen(page)

await expect(page.locator('#diagnostics-component')).toContainText('TypeScript found')
await expect(page.locator('#diagnostics-styles')).toContainText(
'Biome reported issues.',
)

await setComponentEditorSource(
page,
[
'const count: string = "ok"',
'const App = () => <button type="button">Inactive</button>',
].join('\n'),
)

await expect(page.locator('#diagnostics-component')).not.toContainText('TS2322')
await expect(page.locator('#diagnostics-styles')).toContainText(
'Source changed. Click Lint to run diagnostics.',
)
await expect(page.locator('#diagnostics-styles')).not.toContainText(
'Biome reported issues.',
)
})

test('reset lint on styles tab clears in-flight component lint state', async ({
page,
}) => {
await waitForInitialRender(page)

const heavyLintSource = [
...Array.from({ length: 120 }, (_, index) => `const unused${index} = ${index}`),
'const App = () => <button>component tab</button>',
].join('\n')

await setComponentEditorSource(page, heavyLintSource)

await ensureDiagnosticsDrawerOpen(page)
await page.getByRole('button', { name: 'Lint' }).first().click()

await page.getByRole('button', { name: 'Open tab app.css' }).click()
await page.locator('#diagnostics-clear-styles').click()

await expect(page.getByRole('button', { name: /^Diagnostics/ })).toHaveAttribute(
'aria-busy',
'false',
)
await expect(page.locator('#diagnostics-styles')).toContainText('No diagnostics yet.')
await expect(page.locator('#diagnostics-styles')).not.toContainText(
'Biome reported issues.',
)
})
54 changes: 53 additions & 1 deletion src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
createWorkspaceContextSnapshotGetter,
toStyleModeForTabLanguage,
} from './modules/app-core/workspace-local-helpers.js'
import { createDiagnosticsTabStateHelpers } from './modules/app-core/diagnostics-tab-state-helpers.js'
import { createWorkspaceEditorHelpers } from './modules/app-core/workspace-editor-helpers.js'
import { createEditedIndicatorVisibilityController } from './modules/app-core/edited-indicator-visibility-controller.js'
import { createPublishTrailingNewlineNormalizer } from './modules/app-core/publish-trailing-newline-normalizer.js'
Expand Down Expand Up @@ -187,6 +188,14 @@ const diagnosticsClearStyles = document.getElementById('diagnostics-clear-styles
const diagnosticsClearAll = document.getElementById('diagnostics-clear-all')
const diagnosticsComponent = document.getElementById('diagnostics-component')
const diagnosticsStyles = document.getElementById('diagnostics-styles')
const diagnosticsComponentSection = document.querySelector(
'[data-diagnostics-scope="component"]',
)
const diagnosticsStylesSection = document.querySelector(
'[data-diagnostics-scope="styles"]',
)
const diagnosticsComponentHeading = diagnosticsComponentSection?.querySelector('h3')
const diagnosticsStylesHeading = diagnosticsStylesSection?.querySelector('h3')
const appToast = document.getElementById('app-toast')
const previewBgColorInput = document.getElementById('preview-bg-color')
const clearConfirmDialog = document.getElementById('clear-confirm-dialog')
Expand Down Expand Up @@ -224,6 +233,7 @@ cssEditor.value = defaultCss
let previewHost = document.getElementById('preview-host')
let jsxCodeEditor = null
let cssCodeEditor = null
let diagnosticsFlowController = null
let getJsxSource = () => jsxEditor.value
let getCssSource = () => cssEditor.value
let renderRuntime = null
Expand Down Expand Up @@ -647,11 +657,40 @@ const workspaceSyncController = createWorkspaceSyncController({
const getLoadedComponentWorkspaceTab = () =>
workspaceTabsState.getTab(loadedComponentTabId) ?? getWorkspaceTabByKind('component')

const getLoadedStylesWorkspaceTab = () =>
workspaceTabsState.getTab(loadedStylesTabId) ?? getWorkspaceTabByKind('styles')

const getTypecheckSourcePath = () => {
const loadedComponentTab = getLoadedComponentWorkspaceTab()
return toNonEmptyWorkspaceText(loadedComponentTab?.path) || defaultComponentTabPath
}

const {
clearDiagnosticsOnTabSwitch,
getComponentLintTarget,
getStylesLintTarget,
syncDiagnosticsDrawerLayout,
} = createDiagnosticsTabStateHelpers({
getActiveWorkspaceTab,
getLoadedComponentWorkspaceTab,
getLoadedStylesWorkspaceTab,
getTabKind,
toNonEmptyWorkspaceText,
diagnosticsComponentSection,
diagnosticsStylesSection,
diagnosticsComponentHeading,
diagnosticsStylesHeading,
diagnosticsClearComponent,
diagnosticsClearStyles,
diagnosticsClearAll,
clearAllDiagnostics,
setTypeDiagnosticsPending,
setLintDiagnosticsPending,
statusNode,
setStatus,
getDiagnosticsFlowController: () => diagnosticsFlowController,
})

const createWorkspaceTabId = prefix => createWorkspaceTabIdFactory(prefix)

const makeUniqueTabPath = ({ basePath, suffix = '' }) =>
Expand Down Expand Up @@ -734,6 +773,13 @@ const {
},
persistRenderMode: mode => persistRenderMode(mode),
getActiveWorkspaceTab,
onActiveWorkspaceTabChange: (_tab, { changed } = {}) => {
syncDiagnosticsDrawerLayout()

if (changed) {
clearDiagnosticsOnTabSwitch()
}
},
loadWorkspaceTabIntoEditor,
updateRenderModeEditability: () => updateRenderModeEditability(),
getHasCompletedInitialWorkspaceBootstrap: () => hasCompletedInitialWorkspaceBootstrap,
Expand Down Expand Up @@ -1223,6 +1269,8 @@ const runtimeCoreOptions = createRuntimeCoreOptions({
getJsxSource: () => getJsxSource(),
getCssSource: () => getCssSource(),
getTypecheckSourcePath,
getComponentLintTarget,
getStylesLintTarget,
buildWorkspaceTabsSnapshot,
renderMode,
styleMode,
Expand Down Expand Up @@ -1267,7 +1315,7 @@ const runtimeCoreOptions = createRuntimeCoreOptions({
})
const runtimeCore = createRuntimeCoreSetup(runtimeCoreOptions)

const diagnosticsFlowController = runtimeCore.diagnosticsFlowController
diagnosticsFlowController = runtimeCore.diagnosticsFlowController
renderRuntime = runtimeCore.renderRuntime
const setCdnLoading = runtimeCore.setCdnLoading
const typeDiagnostics = diagnosticsFlowController.typeDiagnostics
Expand Down Expand Up @@ -1360,6 +1408,8 @@ bindAppEventsAndStart({
renderPreview,
setJsxSource,
setCssSource,
persistActiveTabEditorContent,
getWorkspaceTabsSnapshot: () => workspaceTabsState.getTabs(),
queueWorkspaceSave,
maybeRender,
maybeRenderFromComponentEditorChange,
Expand Down Expand Up @@ -1389,6 +1439,7 @@ bindAppEventsAndStart({
updateRenderModeEditability,
loadPreferredWorkspaceContext,
getActiveWorkspaceTab,
getTabKind,
setActiveWorkspaceTab,
workspaceTabsState,
loadedStylesTabIdRef: {
Expand All @@ -1397,6 +1448,7 @@ bindAppEventsAndStart({
},
},
getWorkspaceTabByKind,
syncDiagnosticsDrawerLayout,
workspaceSaveController,
workspaceStorage,
bindWorkspaceMetadataPersistence,
Expand Down
Loading
Loading