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
24 changes: 21 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ jobs:
with:
go-version-file: ws/.go-version
# 3. Clone app + core (the PR's PACKAGE is already checked out into its
# slot; bootstrap --tooling skips dirs that already exist). A sibling
# slot; bootstrap --assemble-only skips dirs that already exist). A sibling
# only needs app + core + itself to typecheck + unit-test.
- name: Assemble workspace (app + core + drive)
working-directory: ws
run: TINYCLD_REPO_BASE=https://github.com/tinycld npx @tinycld/bootstrap@latest --tooling --with drive
run: TINYCLD_REPO_BASE=https://github.com/tinycld npx @tinycld/bootstrap@latest --assemble-only --with drive
- name: Install (workspace root)
working-directory: ws
run: npm install
Expand Down Expand Up @@ -71,7 +71,7 @@ jobs:
go-version-file: ws/.go-version
- name: Assemble workspace (app + core + drive)
working-directory: ws
run: TINYCLD_REPO_BASE=https://github.com/tinycld npx @tinycld/bootstrap@latest --tooling --with drive
run: TINYCLD_REPO_BASE=https://github.com/tinycld npx @tinycld/bootstrap@latest --assemble-only --with drive
- name: Install (workspace root)
working-directory: ws
run: npm install
Expand All @@ -80,4 +80,22 @@ jobs:
run: npx playwright install --with-deps chromium
- name: E2E
working-directory: ws/${{ env.PACKAGE }}
env:
# Pre-warm calc's lazy screen chunk in globalSetup so the
# first test per worker doesn't race the cold Metro compile.
# See tinycld/app#18.
TINYCLD_WARM_PACKAGES: calc
run: npx tinycld-pkg test:e2e
# Capture Playwright's per-failure artifacts (trace, screenshot,
# video, error-context.md) so a CI failure can be diagnosed
# without a re-run. `if: failure()` skips the upload on green
# runs to save bandwidth + retention.
- name: Upload Playwright artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-artifacts
path: |
ws/${{ env.PACKAGE }}/test-results/
ws/${{ env.PACKAGE }}/playwright-report/
retention-days: 14
12 changes: 11 additions & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,14 @@ import appConfig from '../app/playwright.config'
const WS_ROOT = path.resolve(import.meta.dirname, '..')
const TEST_DIR = path.join(WS_ROOT, 'node_modules', '@tinycld', 'calc', 'tests')

export default defineConfig({ ...appConfig, testDir: TEST_DIR })
export default defineConfig({
...appConfig,
testDir: TEST_DIR,
// Per-test timeout. Default is 30s; calc tests routinely open xlsx
// files (the seeded Team Scorecard, blank workbooks) where the
// grid hydration + xlsx parse pipeline runs end-to-end inside the
// test body. On CI under parallel load that pipeline can take
// 30-60s for the first navigation per worker. 60s gives the spec
// body room after the navigation overhead.
timeout: 60_000,
})
65 changes: 43 additions & 22 deletions tests/calc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,33 @@ test.describe('Calc', () => {
// The seeded Team Scorecard.xlsx no longer appears on calc's index
// (which is now a panel with three CTAs, not a recent-files list).
// Browse to drive's recent view to find it and click through.
// Drive rows on the recent view open a preview pane on single
// click rather than navigating to the package editor. Use the
// row's context menu's "Open in Calc" action to bypass the
// preview and land directly in the calc editor.
await page.goto(`/a/${ORG_SLUG}/drive/recent`)
await page.getByText('Team Scorecard.xlsx').click()

await expect(page.getByText('Name', { exact: true })).toBeVisible()
await expect(page.getByText('Role', { exact: true })).toBeVisible()
await expect(page.getByText('Score', { exact: true })).toBeVisible()
await page.getByText('Team Scorecard.xlsx').click({ button: 'right' })
await page.getByRole('menuitem', { name: 'Open in Calc' }).click()
await page.waitForURL(/\/calc\/[^/]+$/, { timeout: 30_000 })

// Header row mounts as the xlsx parse + grid hydration completes.
// Header cells appear one-by-one as the xlsx parser yields each
// column to the renderer; on CI under parallel load the gap
// between cells can exceed the default 5s, so each header gets
// its own generous timeout instead of relying on the first one
// to land all three in the same frame.
// Cell A1 / B1 / C1 are uniquely labelled by aria-label rather
// than relying on the inner text — text 'Name' also matches the
// virtualized recent-files "Sort by Name" muted-text header.
await expect(page.getByLabel('Cell A1', { exact: true })).toHaveText('Name', {
timeout: 30_000,
})
await expect(page.getByLabel('Cell B1', { exact: true })).toHaveText('Role', {
timeout: 15_000,
})
await expect(page.getByLabel('Cell C1', { exact: true })).toHaveText('Score', {
timeout: 15_000,
})

await expect(page.getByText('Alice', { exact: true })).toBeVisible()
await expect(page.getByText('Engineer', { exact: true })).toBeVisible()
Expand All @@ -27,23 +48,19 @@ test.describe('Calc', () => {
await expect(page.getByText('Carol', { exact: true })).toBeVisible()
await expect(page.getByText('Manager', { exact: true })).toBeVisible()

// Verify columns are correctly aligned by reading the DOM. A1
// (Name) and B1 (Role) should be at viewport-x positions exactly
// CELL_WIDTH (96px) apart.
// Verify columns are correctly aligned by reading the DOM
// through the stable Cell-A1/B1/C1 aria-labels (not text content,
// which also matches the recent-view "Sort by Name" header
// outside the grid). A1 and B1 should be at viewport-x
// positions exactly CELL_WIDTH (96px) apart.
const positions = await page.evaluate(() => {
const find = (text: string) => {
for (const el of Array.from(document.querySelectorAll('div'))) {
if (el.textContent === text && el.children.length === 0) {
const cell = el.parentElement
if (cell) {
const rect = cell.getBoundingClientRect()
return { left: rect.left, width: rect.width }
}
}
}
return null
const find = (label: string) => {
const el = document.querySelector(`[aria-label="${label}"]`) as HTMLElement | null
if (!el) return null
const rect = el.getBoundingClientRect()
return { left: rect.left, width: rect.width }
}
return { name: find('Name'), role: find('Role'), score: find('Score') }
return { name: find('Cell A1'), role: find('Cell B1'), score: find('Cell C1') }
})
expect(positions.name).not.toBeNull()
expect(positions.role).not.toBeNull()
Expand Down Expand Up @@ -1682,13 +1699,17 @@ test.describe('Calc CSV import/export', () => {

test('Import CSV creates a new spreadsheet from the file picker', async ({ page }) => {
await navigateToPackage(page, 'calc')
await expect(page.getByRole('heading', { level: 2, name: 'Calc' }).first()).toBeVisible({
// The calc index now renders the shared NoFilePanel; CSV import
// happens via the unified "Upload files" card which accepts
// .xlsx and .csv. The file-picker click triggers the same
// CsvImportDialog as the old standalone "Import CSV" button.
await expect(page.getByRole('heading', { level: 1, name: 'A fresh sheet.' })).toBeVisible({
timeout: 30_000,
})

const csv = 'Title,Count\r\nApples,12\r\nOranges,7'
const fileChooserPromise = page.waitForEvent('filechooser')
await page.getByRole('button', { name: 'Import CSV' }).click()
await page.getByText('Upload files', { exact: true }).click()
const chooser = await fileChooserPromise
await chooser.setFiles({
name: 'fruit.csv',
Expand Down
14 changes: 9 additions & 5 deletions tests/pivot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,14 +484,18 @@ async function seedTwoRegionsTwoRows(
await typeIntoCell(page, formulaBar, 'B3', '20')
}

// Click "New spreadsheet" on the calc index and wait for the workbook
// detail screen to mount. Mirrors the helper in calc.spec.ts — kept
// inline so this spec is self-contained.
// Click "New sheet" on the calc index No-File panel and wait for the
// workbook detail screen to mount. Mirrors the helper in calc.spec.ts
// — kept inline so this spec is self-contained.
async function openNewSpreadsheet(page: Page): Promise<void> {
await expect(page.getByRole('heading', { level: 2, name: 'Calc' }).first()).toBeVisible({
// Wait for the No-File panel's headline to render before clicking
// the create button — handleCreateNew needs useOrgInfo /
// useCurrentUserOrg to resolve first, and if the click races that
// the create silently no-ops and waitForURL hangs.
await expect(page.getByRole('heading', { level: 1, name: 'A fresh sheet.' })).toBeVisible({
timeout: 30_000,
})
await page.getByRole('button', { name: 'New spreadsheet' }).click()
await page.getByRole('button', { name: 'New sheet' }).click()
await page.waitForURL(/\/calc\/[^/]+$/, { timeout: 75_000 })
await expect(page.getByLabel('Cell A1', { exact: true })).toBeVisible({ timeout: 75_000 })
}
Expand Down
Loading