Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ab56255
feat(web): add fullscreen table preview and export actions
techotaku39 Aug 26, 2026
3c18098
fix(web): harden table exports and mobile detection
techotaku39 Aug 26, 2026
9b29c4d
fix(web): address table viewer review findings
techotaku39 Aug 26, 2026
c12f955
fix(web): exclude table controls from shared images
techotaku39 Aug 26, 2026
830e0fa
test(web): stabilize landscape table coverage
techotaku39 Aug 26, 2026
d9ea439
feat(web): complete table preview actions and export fidelity
techotaku39 Aug 27, 2026
13d5c04
fix(web): harden table export and preview coverage
techotaku39 Aug 27, 2026
eec6d66
fix(web): address table preview review findings
techotaku39 Aug 27, 2026
e693ea0
fix(web): harden table preview sizing
techotaku39 Aug 27, 2026
37cc1ba
fix(web): align table actions and export tiles
techotaku39 Aug 27, 2026
01b9d2c
fix(web): preserve table preview sizing
techotaku39 Aug 27, 2026
ee252a3
fix(web): report table image action failures
techotaku39 Aug 27, 2026
e3e1c0a
fix(web): handle pending mobile orientation locks
techotaku39 Aug 27, 2026
e1a961e
fix(web): preserve markdown table cell formatting
techotaku39 Aug 27, 2026
e2f3cea
fix(web): handle synchronous image clipboard errors
techotaku39 Aug 27, 2026
538c12c
fix(web): preserve rich markdown table links
techotaku39 Aug 27, 2026
06a9b3a
fix(web): preserve authored table link targets
techotaku39 Aug 27, 2026
bb6c311
fix(web): close pending table viewer work
techotaku39 Aug 27, 2026
638c0bd
fix(web): escape markdown table text safely
techotaku39 Aug 27, 2026
b21a912
fix(web): round-trip table media text safely
techotaku39 Aug 27, 2026
8dbd458
fix(web): preserve table header cell colors
techotaku39 Aug 27, 2026
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
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
- run: bun typecheck
- run: bunx playwright install --with-deps chromium
- run: bun run test:e2e -- terminal-wrap-fidelity.spec.ts composer-copy.spec.ts
- run: cd web && bunx playwright test --config playwright.config.ts e2e/markdown-table.spec.ts e2e/markdown-table-mobile.spec.ts

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAJOR] Prebundle the remaining Workbox client before making this suite mandatory.

The clean current-head run fails here. Its log reports new dependencies optimized: workbox-window followed by optimized dependencies changed. reloading; that reload tears down the mobile table dialog, and the close-button click times out on a detached element. web/vite.config.ts:183 already prebundles Workbox modules specifically to prevent this E2E reload, but omits workbox-window.

Suggested fix:

optimizeDeps: {
    include: [
        'workbox-precaching',
        'workbox-routing',
        'workbox-strategies',
        'workbox-expiration',
        'workbox-window',
    ],
},

- run: bun run test

# Serial runner-integration suite: starts real detached runner/session
Expand Down
17 changes: 17 additions & 0 deletions web/e2e-fixtures/markdown-table-fixture.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>HAPI Markdown table fixture</title>
<style>
html { background: #fff; }
body { margin: 0; padding: 24px; font-family: system-ui, sans-serif; }
#root { max-width: 960px; margin: 0 auto; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="./markdown-table-fixture.tsx"></script>
</body>
</html>
46 changes: 46 additions & 0 deletions web/e2e-fixtures/markdown-table-fixture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import '../src/index.css'
import { HappyChatProvider, type HappyChatContextValue } from '../src/components/AssistantChat/context'
import { I18nProvider } from '../src/lib/i18n-context'
import { MarkdownRenderer } from '../src/components/MarkdownRenderer'

const TABLE_MARKDOWN = `# Repository activity

| Project | Stars | Language | Latest release | Maintainer | Notes |
| --- | ---: | --- | --- | --- | --- |
| HAPI | 128 | TypeScript | 0.28.0 | Local-first team | Remote control for coding agents |
| HAPI, local-first | 42 | TypeScript | 0.27.3 | Community | A deliberately long description for horizontal table scrolling |
| Example | 7 | Rust | 1.2.0 | Open source | Stable fixture row |`

const NEAR_BOTTOM_VERTICAL_TABLE_MARKDOWN = `# Near bottom table

| Item | Status |
| --- | --- |
${Array.from({ length: 14 }, (_, index) => `| Item ${index + 1} | Ready |`).join('\n')}`

function MarkdownTableFixture() {
const query = new URLSearchParams(window.location.search)
const content = query.has('near-bottom-scroll')
? NEAR_BOTTOM_VERTICAL_TABLE_MARKDOWN
: TABLE_MARKDOWN

return (
<HappyChatProvider value={{ sessionTitle: 'Table filename fixture' } as HappyChatContextValue}>
<main data-testid="markdown-table-fixture">
<MarkdownRenderer standalone content={content} />
</main>
</HappyChatProvider>
)
}

const root = document.getElementById('root')
if (root) {
ReactDOM.createRoot(root).render(
<React.StrictMode>
<I18nProvider>
<MarkdownTableFixture />
</I18nProvider>
</React.StrictMode>,
)
}
124 changes: 124 additions & 0 deletions web/e2e/markdown-table-mobile.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { devices, expect, test } from '@playwright/test'

test.use({ ...devices['Pixel 7'] })

test('mobile markdown table viewer requests landscape and releases orientation controls', async ({ page }) => {
await page.goto('/e2e-fixtures/markdown-table-fixture.html')
await page.evaluate(() => {
const state = { requestFullscreen: 0, exitFullscreen: 0, locks: [] as string[], unlocks: 0 }
Object.defineProperty(window, '__hapiTableViewerState', { configurable: true, value: state })
Object.defineProperty(document.documentElement, 'requestFullscreen', {
configurable: true,
value: () => {
state.requestFullscreen += 1
return Promise.resolve()
},
})
Object.defineProperty(document, 'exitFullscreen', {
configurable: true,
value: () => {
state.exitFullscreen += 1
return Promise.resolve()
},
})
Object.defineProperty(window.screen, 'orientation', {
configurable: true,
value: {
lock: (value: string) => {
state.locks.push(value)
return Promise.resolve()
},
unlock: () => {
state.unlocks += 1
},
},
})
})

const inlineActions = page.locator('[data-testid="markdown-table-fixture"] .aui-md-table-actions')
await expect(inlineActions).toBeVisible()
await expect(inlineActions.getByRole('button')).toHaveCount(1)
await page.getByRole('button', { name: 'Open table full screen' }).click()
const dialog = page.getByRole('dialog', { name: 'Table filename fixture' })
await expect(dialog).toBeVisible()
// A real mobile browser can rotate to a landscape CSS viewport. Keep the
// mobile title unshifted even when its width becomes desktop-sized.
await page.setViewportSize({ width: 915, height: 412 })
const wrapButton = dialog.locator('button[data-hapi-table-wrap-toggle="true"]')
await expect(wrapButton).toBeVisible()
await expect(wrapButton).toHaveAttribute('aria-pressed', /true|false/)
await expect(dialog.getByRole('button', { name: 'Copy table as Markdown' })).toBeVisible()
const downloadButton = dialog.getByRole('button', { name: 'Download table' })
await expect(downloadButton).toBeVisible()
const initiallyWrapped = await wrapButton.getAttribute('aria-pressed')
await wrapButton.click()
await expect(wrapButton).toHaveAttribute('aria-pressed', initiallyWrapped === 'true' ? 'false' : 'true')
await expect.poll(() => dialog.locator('[data-hapi-table-viewer-toolbar="true"]').evaluate((element) => {
const style = getComputedStyle(element)
return `${style.paddingLeft}:${style.paddingRight}:${style.paddingTop}:${style.paddingBottom}`
})).toBe('6px:6px:0px:0px')
await expect.poll(() => dialog.locator('[data-hapi-table-viewer-toolbar="true"]').evaluate((element) => getComputedStyle(element).columnGap)).toBe('4px')
await expect.poll(() => dialog.locator('[data-hapi-table-viewer-heading="true"]').evaluate((element) => getComputedStyle(element).transform)).toBe('none')
await expect.poll(() => dialog.locator('[data-hapi-table-viewer="true"] thead th').first().evaluate((element) => {
const thead = element.closest('thead')
return `${getComputedStyle(thead ?? element).position}:${getComputedStyle(element).position}:${getComputedStyle(element).top}`
})).toBe('static:sticky:0px')
await expect.poll(() => page.evaluate(() => {
const state = (window as Window & { __hapiTableViewerState?: { requestFullscreen: number; locks: string[] } }).__hapiTableViewerState
return state ? `${state.requestFullscreen}:${state.locks.join(',')}` : ''
})).toBe('1:landscape')

await downloadButton.click()
await expect(page.getByRole('menuitem', { name: 'Download PNG' })).toBeVisible()
await expect(page.getByRole('menuitem', { name: 'Download CSV' })).toBeVisible()
const imageDownloadPromise = page.waitForEvent('download')
await page.getByRole('menuitem', { name: 'Download PNG' }).click()
const imageDownload = await imageDownloadPromise
expect(imageDownload.suggestedFilename()).toMatch(/^HAPI Table-Table filename fixture-\d{14}\.png$/)

const csvDownloadPromise = page.waitForEvent('download')
await downloadButton.click()
await page.getByRole('menuitem', { name: 'Download CSV' }).click()
const csvDownload = await csvDownloadPromise
expect(csvDownload.suggestedFilename()).toMatch(/^HAPI Table-Table filename fixture-\d{14}\.csv$/)

await dialog.getByRole('button', { name: 'Close table full screen' }).click()
await expect.poll(() => page.evaluate(() => {
const state = (window as Window & { __hapiTableViewerState?: { exitFullscreen: number; unlocks: number } }).__hapiTableViewerState
return state ? `${state.exitFullscreen}:${state.unlocks}` : ''
})).toBe('1:1')
})

test('mobile markdown table viewer detects a phone that starts in landscape', async ({ page }) => {
await page.setViewportSize({ width: 915, height: 412 })
await page.goto('/e2e-fixtures/markdown-table-fixture.html')
await expect(page.getByRole('button', { name: 'Open table full screen' })).toBeVisible()
await page.evaluate(() => {
const state = { requestFullscreen: 0, locks: [] as string[] }
Object.defineProperty(window, '__hapiTableViewerState', { configurable: true, value: state })
Object.defineProperty(document.documentElement, 'requestFullscreen', {
configurable: true,
value: () => {
state.requestFullscreen += 1
return Promise.resolve()
},
})
Object.defineProperty(window.screen, 'orientation', {
configurable: true,
value: {
lock: (value: string) => {
state.locks.push(value)
return Promise.resolve()
},
unlock: () => {},
},
})
})

await page.getByRole('button', { name: 'Open table full screen' }).click()
await expect(page.getByRole('dialog', { name: 'Table filename fixture' })).toBeVisible()
await expect.poll(() => page.evaluate(() => {
const state = (window as Window & { __hapiTableViewerState?: { requestFullscreen: number; locks: string[] } }).__hapiTableViewerState
return state ? `${state.requestFullscreen}:${state.locks.join(',')}` : ''
})).toBe('1:landscape')
})
Loading
Loading