diff --git a/playwright/app.spec.ts b/playwright/app.spec.ts index 5258370..d56316f 100644 --- a/playwright/app.spec.ts +++ b/playwright/app.spec.ts @@ -74,6 +74,28 @@ const ensurePanelToolsVisible = async (page: Page, panelName: 'component' | 'sty } } +const ensureDiagnosticsDrawerOpen = async (page: Page) => { + const toggle = page.locator('#diagnostics-toggle') + const isExpanded = await toggle.getAttribute('aria-expanded') + + if (isExpanded !== 'true') { + await toggle.click() + } + + await expect(page.locator('#diagnostics-drawer')).toBeVisible() +} + +const ensureDiagnosticsDrawerClosed = async (page: Page) => { + const toggle = page.locator('#diagnostics-toggle') + const isExpanded = await toggle.getAttribute('aria-expanded') + + if (isExpanded === 'true') { + await page.locator('#diagnostics-close').click() + } + + await expect(page.locator('#diagnostics-drawer')).toBeHidden() +} + const expectCollapseButtonState = async ( page: Page, panelName: 'component' | 'styles' | 'preview', @@ -383,7 +405,7 @@ test('react mode typecheck loads types without malformed URL fetches', async ({ await page.locator('#render-mode').selectOption('react') await page.getByRole('button', { name: 'Typecheck' }).click() - await page.locator('#diagnostics-toggle').click() + await ensureDiagnosticsDrawerOpen(page) await expect(page.locator('#diagnostics-component')).toContainText( 'No TypeScript errors found.', ) @@ -558,7 +580,7 @@ test('style compilation errors populate styles diagnostics scope', async ({ page /diagnostics-toggle--error/, ) - await page.locator('#diagnostics-toggle').click() + await ensureDiagnosticsDrawerOpen(page) await expect(page.locator('#diagnostics-styles')).toContainText( 'Style compilation failed.', ) @@ -627,6 +649,7 @@ test('clearing styles keeps diagnostics error state but resets status styling', ) const dialog = page.locator('#clear-confirm-dialog') + await ensureDiagnosticsDrawerClosed(page) await page.getByLabel('Clear styles source').click() await expect(dialog).toHaveAttribute('open', '') await dialog.getByRole('button', { name: 'Clear' }).click() @@ -659,7 +682,7 @@ test('clear component diagnostics removes type errors and restores rendered stat ) await expect(page.locator('#status')).toHaveText(/Rendered \(Type errors: [1-9]\d*\)/) - await page.locator('#diagnostics-toggle').click() + await ensureDiagnosticsDrawerOpen(page) await page.locator('#diagnostics-clear-component').click() await expect(page.locator('#diagnostics-component')).toContainText( @@ -685,7 +708,7 @@ test('clear all diagnostics removes style compile diagnostics', async ({ page }) /diagnostics-toggle--error/, ) - await page.locator('#diagnostics-toggle').click() + await ensureDiagnosticsDrawerOpen(page) await expect(page.locator('#diagnostics-styles')).toContainText( 'Style compilation failed.', ) @@ -713,7 +736,7 @@ test('clear styles diagnostics removes style compile diagnostics', async ({ page /diagnostics-toggle--error/, ) - await page.locator('#diagnostics-toggle').click() + await ensureDiagnosticsDrawerOpen(page) await expect(page.locator('#diagnostics-styles')).toContainText( 'Style compilation failed.', ) @@ -740,7 +763,7 @@ test('typecheck success reports ok diagnostics state in button and drawer', asyn await expect(page.locator('#diagnostics-toggle')).toHaveClass(/diagnostics-toggle--ok/) await expect(page.locator('#diagnostics-toggle')).toHaveText('Diagnostics') - await page.locator('#diagnostics-toggle').click() + await ensureDiagnosticsDrawerOpen(page) await expect(page.locator('#diagnostics-component')).toContainText( 'No TypeScript errors found.', ) @@ -766,7 +789,7 @@ test('typecheck error reports diagnostics count in button and details in drawer' ) await expect(page.locator('#diagnostics-toggle')).toHaveText(/Diagnostics \([1-9]\d*\)/) - await page.locator('#diagnostics-toggle').click() + await expect(page.locator('#diagnostics-drawer')).toBeVisible() await expect(page.locator('#diagnostics-component')).toContainText('TypeScript found') await expect(page.locator('#diagnostics-component')).toContainText('TS') }) @@ -787,7 +810,7 @@ test('component diagnostics rows navigate editor to reported line', async ({ pag await expect(page.locator('#diagnostics-toggle')).toHaveClass( /diagnostics-toggle--error/, ) - await page.locator('#diagnostics-toggle').click() + await ensureDiagnosticsDrawerOpen(page) const targetDiagnostic = page .locator('#diagnostics-component .diagnostic-line-button[data-diagnostic-line="2"]') @@ -817,7 +840,7 @@ test('component diagnostics support arrow navigation and enter jump', async ({ /diagnostics-toggle--error/, ) - await page.locator('#diagnostics-toggle').click() + await ensureDiagnosticsDrawerOpen(page) const firstDiagnostic = page .locator('#diagnostics-component .diagnostic-line-button') @@ -854,7 +877,7 @@ test('component lint error reports diagnostics count and details', async ({ page ) await expect(page.locator('#diagnostics-toggle')).toHaveText(/Diagnostics \([1-9]\d*\)/) - await page.locator('#diagnostics-toggle').click() + await expect(page.locator('#diagnostics-drawer')).toBeVisible() await expect(page.locator('#diagnostics-component')).toContainText( 'Biome reported issues.', ) @@ -874,7 +897,7 @@ test('styles diagnostics rows navigate editor to reported line', async ({ page } await expect(page.locator('#diagnostics-toggle')).toHaveClass( /diagnostics-toggle--error/, ) - await page.locator('#diagnostics-toggle').click() + await ensureDiagnosticsDrawerOpen(page) const targetDiagnostic = page .locator('#diagnostics-styles .diagnostic-line-button[data-diagnostic-line="3"]') @@ -904,7 +927,7 @@ test('clear component diagnostics resets rendered lint-issue status pill', async await expect(page.locator('#status')).toHaveText(/Rendered \(Lint issues: [1-9]\d*\)/) await expect(page.locator('#status')).toHaveClass(/status--error/) - await page.locator('#diagnostics-toggle').click() + await ensureDiagnosticsDrawerOpen(page) await page.locator('#diagnostics-clear-component').click() await expect(page.locator('#diagnostics-component')).toContainText( @@ -932,7 +955,7 @@ test('component lint ignores unused App View and render bindings', async ({ page await runComponentLint(page) - await page.locator('#diagnostics-toggle').click() + await ensureDiagnosticsDrawerOpen(page) await expect(page.locator('#diagnostics-component')).toContainText( 'No Biome issues found.', ) @@ -1018,7 +1041,7 @@ test('changing css dialect resets diagnostics after lint and typecheck runs', as ) await expect(page.locator('#diagnostics-toggle')).toHaveText('Diagnostics') - await page.locator('#diagnostics-toggle').click() + await ensureDiagnosticsDrawerOpen(page) await expect(page.locator('#diagnostics-component')).toContainText( 'No diagnostics yet.', ) diff --git a/src/app.js b/src/app.js index 236f535..9926d41 100644 --- a/src/app.js +++ b/src/app.js @@ -504,6 +504,11 @@ const typeDiagnostics = createTypeDiagnosticsController({ incrementTypeDiagnosticsRuns, decrementTypeDiagnosticsRuns, getActiveTypeDiagnosticsRuns, + onIssuesDetected: ({ issueCount }) => { + if (issueCount > 0) { + setDiagnosticsDrawerOpen(true) + } + }, }) const lintDiagnostics = createLintDiagnosticsController({ @@ -515,6 +520,11 @@ const lintDiagnostics = createLintDiagnosticsController({ setComponentDiagnostics: setTypeDiagnosticsDetails, setStyleDiagnostics: setStyleDiagnosticsDetails, setStatus, + onIssuesDetected: ({ issueCount }) => { + if (issueCount > 0) { + setDiagnosticsDrawerOpen(true) + } + }, }) let activeComponentLintAbortController = null @@ -544,7 +554,7 @@ const syncLintPendingState = () => { setLintDiagnosticsPending(componentLintPending || stylesLintPending) } -const runComponentLint = async () => { +const runComponentLint = async ({ userInitiated = false } = {}) => { activeComponentLintAbortController?.abort() const controller = new AbortController() activeComponentLintAbortController = controller @@ -557,6 +567,7 @@ const runComponentLint = async () => { try { const result = await lintDiagnostics.lintComponent({ signal: controller.signal, + userInitiated, }) if (result) { lastComponentLintIssueCount = result.issueCount @@ -570,7 +581,7 @@ const runComponentLint = async () => { } } -const runStylesLint = async () => { +const runStylesLint = async ({ userInitiated = false } = {}) => { activeStylesLintAbortController?.abort() const controller = new AbortController() activeStylesLintAbortController = controller @@ -583,6 +594,7 @@ const runStylesLint = async () => { try { const result = await lintDiagnostics.lintStyles({ signal: controller.signal, + userInitiated, }) if (result) { lastStylesLintIssueCount = result.issueCount @@ -901,17 +913,17 @@ if (diagnosticsClearAll) { } if (typecheckButton) { typecheckButton.addEventListener('click', () => { - typeDiagnostics.triggerTypeDiagnostics() + typeDiagnostics.triggerTypeDiagnostics({ userInitiated: true }) }) } if (lintComponentButton) { lintComponentButton.addEventListener('click', () => { - void runComponentLint() + void runComponentLint({ userInitiated: true }) }) } if (lintStylesButton) { lintStylesButton.addEventListener('click', () => { - void runStylesLint() + void runStylesLint({ userInitiated: true }) }) } renderButton.addEventListener('click', renderPreview) diff --git a/src/modules/lint-diagnostics.js b/src/modules/lint-diagnostics.js index 90a2770..8e91c17 100644 --- a/src/modules/lint-diagnostics.js +++ b/src/modules/lint-diagnostics.js @@ -210,6 +210,7 @@ export const createLintDiagnosticsController = ({ setComponentDiagnostics, setStyleDiagnostics, setStatus, + onIssuesDetected = () => {}, }) => { let biomeWorkspacePromise = null let componentLintRunId = 0 @@ -296,7 +297,7 @@ export const createLintDiagnosticsController = ({ } } - const lintComponent = async ({ signal } = {}) => { + const lintComponent = async ({ signal, userInitiated = false } = {}) => { componentLintRunId += 1 const runId = componentLintRunId @@ -331,6 +332,15 @@ export const createLintDiagnosticsController = ({ : 'Rendered', summary.level === 'error' ? 'error' : 'neutral', ) + + if (userInitiated && summary.lines.length > 0) { + onIssuesDetected({ + kind: 'lint', + scope: 'component', + issueCount: summary.lines.length, + }) + } + return { issueCount: summary.lines.length, } @@ -356,7 +366,7 @@ export const createLintDiagnosticsController = ({ } } - const lintStyles = async ({ signal } = {}) => { + const lintStyles = async ({ signal, userInitiated = false } = {}) => { stylesLintRunId += 1 const runId = stylesLintRunId @@ -403,6 +413,15 @@ export const createLintDiagnosticsController = ({ : 'Rendered', summary.level === 'error' ? 'error' : 'neutral', ) + + if (userInitiated && summary.lines.length > 0) { + onIssuesDetected({ + kind: 'lint', + scope: 'styles', + issueCount: summary.lines.length, + }) + } + return { issueCount: summary.lines.length, } diff --git a/src/modules/type-diagnostics.js b/src/modules/type-diagnostics.js index 6fc7331..33a08dd 100644 --- a/src/modules/type-diagnostics.js +++ b/src/modules/type-diagnostics.js @@ -230,6 +230,7 @@ export const createTypeDiagnosticsController = ({ incrementTypeDiagnosticsRuns, decrementTypeDiagnosticsRuns, getActiveTypeDiagnosticsRuns, + onIssuesDetected = () => {}, }) => { let typeCheckRunId = 0 let typeScriptCompiler = null @@ -840,7 +841,7 @@ export const createTypeDiagnosticsController = ({ .filter(diagnostic => !shouldIgnoreTypeDiagnostic(diagnostic)) } - const runTypeDiagnostics = async runId => { + const runTypeDiagnostics = async (runId, { userInitiated = false } = {}) => { incrementTypeDiagnosticsRuns() setTypeDiagnosticsPending(false) setTypecheckButtonLoading(true) @@ -879,6 +880,14 @@ export const createTypeDiagnosticsController = ({ level: 'error', }) setStatus(`Rendered (Type errors: ${errors.length})`, 'error') + + if (userInitiated) { + onIssuesDetected({ + kind: 'type', + scope: 'component', + issueCount: errors.length, + }) + } } if (isRenderedStatus()) { @@ -911,9 +920,9 @@ export const createTypeDiagnosticsController = ({ } } - const triggerTypeDiagnostics = () => { + const triggerTypeDiagnostics = ({ userInitiated = false } = {}) => { typeCheckRunId += 1 - void runTypeDiagnostics(typeCheckRunId) + void runTypeDiagnostics(typeCheckRunId, { userInitiated }) } const scheduleTypeRecheck = () => { diff --git a/src/styles/diagnostics.css b/src/styles/diagnostics.css index d04b944..225cf7f 100644 --- a/src/styles/diagnostics.css +++ b/src/styles/diagnostics.css @@ -218,7 +218,7 @@ top: auto; right: 12px; left: 12px; - bottom: 68px; + bottom: 12px; width: auto; max-height: 58vh; }