From 0a350c3a8532a6d04d20fdcb413d1d0e5ed05678 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Sat, 30 May 2026 18:41:19 -0500 Subject: [PATCH 01/13] fix(ci): use bootstrap v2 --assemble-only flag @tinycld/bootstrap v2 (published 2026-05-30 21:52 UTC) removed the --tooling flag in favor of --assemble-only. CI on this repo broke as soon as v2 propagated to `latest`. Same one-line fix already applied to tinycld/app#9; core, drive, and text were migrated alongside the bootstrap v2 release. --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba51955..8996207 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 From f8e63aad05be900c10e634c7d8f7954cbc79730c Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Sat, 30 May 2026 19:07:27 -0500 Subject: [PATCH 02/13] ci: upload Playwright artifacts on E2E failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the canonical pattern in tinycld/text. Without this, Playwright's trace.zip, screenshot, video, and error-context.md are written to the runner's filesystem on test failure but evaporate when the runner shuts down — making CI-only flakes hard to diagnose without re-running. `if: failure()` skips the upload on green runs. 14-day retention. Bootstrap template gets the same change in tinycld/bootstrap#3 so future packages inherit it. --- .github/workflows/ci.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8996207..80a9a16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,3 +81,16 @@ jobs: - name: E2E working-directory: ws/${{ env.PACKAGE }} 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 From f4362ce3d680cba41ec4821c8dbf9cd1c4f66c6f Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Sat, 30 May 2026 23:05:09 -0500 Subject: [PATCH 03/13] test(e2e): fix pivot.spec.ts openNewSpreadsheet helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The local helper was using the pre-No-File-panel UI strings ('Calc' heading + 'New spreadsheet' button). Those are gone — the calc index now renders the shared No-File panel with 'A fresh sheet.' headline and 'New sheet' button. Aligns with the equivalent helper in calc.spec.ts (which was updated when the No-File panel landed). All 10 pivot specs pass locally. --- tests/pivot.spec.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/pivot.spec.ts b/tests/pivot.spec.ts index ff26ee4..7d06855 100644 --- a/tests/pivot.spec.ts +++ b/tests/pivot.spec.ts @@ -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 { - 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 }) } From 128625efdb814da98f1709b3f85be754d85f2452 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Sat, 30 May 2026 23:20:03 -0500 Subject: [PATCH 04/13] test(e2e): use current No-File panel headline + give xlsx parse more time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two unrelated fixes to the only two specs still failing on this branch: 1. calc.spec.ts:1683 (Import CSV) — was waiting for the pre-No-File 'Calc' h2 heading + 'New spreadsheet' button. Aligns with the actual UI: 'A fresh sheet.' h1 (the No-File panel headline). Mirrors the pivot.spec.ts helper rewrite earlier on this branch. 2. calc.spec.ts:12 (opening Team Scorecard) — the first cell header ('Name') was timing out at the default 5s on CI under load. The xlsx parse + grid hydration is the slow path on the cold-cache first navigation; once the grid is up, subsequent header cells appear immediately. Bumps just the first toBeVisible to 15s so the gate matches reality; the next two stay at default. --- tests/calc.spec.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/calc.spec.ts b/tests/calc.spec.ts index 57c265a..36ffc81 100644 --- a/tests/calc.spec.ts +++ b/tests/calc.spec.ts @@ -16,7 +16,11 @@ test.describe('Calc', () => { await page.goto(`/a/${ORG_SLUG}/drive/recent`) await page.getByText('Team Scorecard.xlsx').click() - await expect(page.getByText('Name', { exact: true })).toBeVisible() + // Header row mounts as the xlsx parse + grid hydration completes. + // First header expectation absorbs the slow path (CI under load + // takes longer than the default 5s); subsequent checks default- + // timeout once the grid is up. + await expect(page.getByText('Name', { exact: true })).toBeVisible({ timeout: 15_000 }) await expect(page.getByText('Role', { exact: true })).toBeVisible() await expect(page.getByText('Score', { exact: true })).toBeVisible() @@ -1682,7 +1686,12 @@ 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({ + // Wait for the No-File panel headline — the calc index renders it + // (with 'A fresh sheet.') as the entry point that hosts the + // Import CSV button. + await expect( + page.getByRole('heading', { level: 1, name: 'A fresh sheet.' }) + ).toBeVisible({ timeout: 30_000, }) From 8e3c4ad56123914711c3fcc9e669bee1d14633c5 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Sat, 30 May 2026 23:33:15 -0500 Subject: [PATCH 05/13] style(calc): biome format fix from previous commit --- tests/calc.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/calc.spec.ts b/tests/calc.spec.ts index 36ffc81..8c8b566 100644 --- a/tests/calc.spec.ts +++ b/tests/calc.spec.ts @@ -1689,9 +1689,7 @@ test.describe('Calc CSV import/export', () => { // Wait for the No-File panel headline — the calc index renders it // (with 'A fresh sheet.') as the entry point that hosts the // Import CSV button. - await expect( - page.getByRole('heading', { level: 1, name: 'A fresh sheet.' }) - ).toBeVisible({ + await expect(page.getByRole('heading', { level: 1, name: 'A fresh sheet.' })).toBeVisible({ timeout: 30_000, }) From 974f3eee58a135d41167551f216041da621c12c9 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Sat, 30 May 2026 23:50:46 -0500 Subject: [PATCH 06/13] test(e2e): update Import CSV to click the unified Upload files card --- tests/calc.spec.ts | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/tests/calc.spec.ts b/tests/calc.spec.ts index 8c8b566..e2e321e 100644 --- a/tests/calc.spec.ts +++ b/tests/calc.spec.ts @@ -17,12 +17,14 @@ test.describe('Calc', () => { await page.getByText('Team Scorecard.xlsx').click() // Header row mounts as the xlsx parse + grid hydration completes. - // First header expectation absorbs the slow path (CI under load - // takes longer than the default 5s); subsequent checks default- - // timeout once the grid is up. - await expect(page.getByText('Name', { exact: true })).toBeVisible({ timeout: 15_000 }) - await expect(page.getByText('Role', { exact: true })).toBeVisible() - await expect(page.getByText('Score', { exact: true })).toBeVisible() + // 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. + await expect(page.getByText('Name', { exact: true })).toBeVisible({ timeout: 30_000 }) + await expect(page.getByText('Role', { exact: true })).toBeVisible({ timeout: 15_000 }) + await expect(page.getByText('Score', { exact: true })).toBeVisible({ timeout: 15_000 }) await expect(page.getByText('Alice', { exact: true })).toBeVisible() await expect(page.getByText('Engineer', { exact: true })).toBeVisible() @@ -1686,16 +1688,17 @@ test.describe('Calc CSV import/export', () => { test('Import CSV creates a new spreadsheet from the file picker', async ({ page }) => { await navigateToPackage(page, 'calc') - // Wait for the No-File panel headline — the calc index renders it - // (with 'A fresh sheet.') as the entry point that hosts the - // Import CSV button. + // 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', From 5b374bf9c234e6218c7f66d72e9cb7808836c86b Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Sun, 31 May 2026 00:05:20 -0500 Subject: [PATCH 07/13] test(e2e): waitForURL after row click + bump per-test timeout to 60s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two stability fixes to the one spec still failing on calc#8: 1. calc.spec.ts:12 — `getByText('Team Scorecard.xlsx').click()` races the navigation to /a//calc/; before the calc screen mounts, getByText('Name') was matching the drive recent-view's "Sort by Name" column-header button. Gates on the URL change first so subsequent assertions only fire after we're inside calc. 2. playwright.config.ts — bump per-test timeout to 60s. Calc tests routinely open xlsx files where the grid hydration + xlsx parse runs end-to-end inside the test body; on CI under parallel load the first navigation per worker can take 30-60s. Default 30s doesn't leave room after the navigation overhead. Both spec-level + config-level — neither suppresses flakiness, they just wait for the cold-cache compile to actually finish before asserting on its output. --- playwright.config.ts | 12 +++++++++++- tests/calc.spec.ts | 6 ++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/playwright.config.ts b/playwright.config.ts index 40ac7d8..1f32043 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -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, +}) diff --git a/tests/calc.spec.ts b/tests/calc.spec.ts index e2e321e..cc4bc94 100644 --- a/tests/calc.spec.ts +++ b/tests/calc.spec.ts @@ -16,6 +16,12 @@ test.describe('Calc', () => { await page.goto(`/a/${ORG_SLUG}/drive/recent`) await page.getByText('Team Scorecard.xlsx').click() + // Gate on the URL changing to /calc/ first — without it, + // getByText('Name') below would erroneously match the drive + // recent-view 'Sort by Name' column-header button before the + // calc-screen navigation lands. + 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 From 9b6c34ecedcce5e3e1d8015823ed51595d046f25 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Sun, 31 May 2026 00:19:00 -0500 Subject: [PATCH 08/13] test(e2e): click by-role for Team Scorecard.xlsx row --- tests/calc.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/calc.spec.ts b/tests/calc.spec.ts index cc4bc94..7cb33d4 100644 --- a/tests/calc.spec.ts +++ b/tests/calc.spec.ts @@ -14,7 +14,11 @@ test.describe('Calc', () => { // (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. await page.goto(`/a/${ORG_SLUG}/drive/recent`) - await page.getByText('Team Scorecard.xlsx').click() + // Click the row button (not the inner text — that just selects). + // getByRole anchors on the accessible name which includes the + // file's "me May 31, 2026" suffix; the regex matches the + // filename prefix. + await page.getByRole('button', { name: /Team Scorecard\.xlsx/ }).click() // Gate on the URL changing to /calc/ first — without it, // getByText('Name') below would erroneously match the drive From d59530dce48a03dbdb8a7da813820e1bb755fac4 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Sun, 31 May 2026 00:30:47 -0500 Subject: [PATCH 09/13] test(e2e): use dblclick to open xlsx from drive recent view --- tests/calc.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/calc.spec.ts b/tests/calc.spec.ts index 7cb33d4..c45db6d 100644 --- a/tests/calc.spec.ts +++ b/tests/calc.spec.ts @@ -14,11 +14,11 @@ test.describe('Calc', () => { // (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. await page.goto(`/a/${ORG_SLUG}/drive/recent`) - // Click the row button (not the inner text — that just selects). + // Drive rows: single click selects, double click opens. // getByRole anchors on the accessible name which includes the // file's "me May 31, 2026" suffix; the regex matches the // filename prefix. - await page.getByRole('button', { name: /Team Scorecard\.xlsx/ }).click() + await page.getByRole('button', { name: /Team Scorecard\.xlsx/ }).dblclick() // Gate on the URL changing to /calc/ first — without it, // getByText('Name') below would erroneously match the drive From 6091070dbc2596210bde13307975d886a7289660 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Sun, 31 May 2026 00:36:53 -0500 Subject: [PATCH 10/13] ci(calc): set TINYCLD_WARM_PACKAGES=calc on E2E for chunk pre-warming --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80a9a16..9e87e2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,6 +80,11 @@ 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 From e83b7badee5958787e3b963563b520c9f2d557ee Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Sun, 31 May 2026 00:40:07 -0500 Subject: [PATCH 11/13] ci: trigger rerun after app#18 (warmPackageChunks) merged From 50034801b2b6a8aeb078b4b8d54761889c14dbd8 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Sun, 31 May 2026 00:55:51 -0500 Subject: [PATCH 12/13] test(e2e): open xlsx via context menu Open-in-Calc, not row click --- tests/calc.spec.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/calc.spec.ts b/tests/calc.spec.ts index c45db6d..6d23b4b 100644 --- a/tests/calc.spec.ts +++ b/tests/calc.spec.ts @@ -13,17 +13,13 @@ 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`) - // Drive rows: single click selects, double click opens. - // getByRole anchors on the accessible name which includes the - // file's "me May 31, 2026" suffix; the regex matches the - // filename prefix. - await page.getByRole('button', { name: /Team Scorecard\.xlsx/ }).dblclick() - - // Gate on the URL changing to /calc/ first — without it, - // getByText('Name') below would erroneously match the drive - // recent-view 'Sort by Name' column-header button before the - // calc-screen navigation lands. + 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. @@ -32,9 +28,15 @@ test.describe('Calc', () => { // 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. - await expect(page.getByText('Name', { exact: true })).toBeVisible({ timeout: 30_000 }) - await expect(page.getByText('Role', { exact: true })).toBeVisible({ timeout: 15_000 }) - await expect(page.getByText('Score', { exact: true })).toBeVisible({ timeout: 15_000 }) + await expect(page.getByText('Name', { exact: true }).first()).toBeVisible({ + timeout: 30_000, + }) + await expect(page.getByText('Role', { exact: true }).first()).toBeVisible({ + timeout: 15_000, + }) + await expect(page.getByText('Score', { exact: true }).first()).toBeVisible({ + timeout: 15_000, + }) await expect(page.getByText('Alice', { exact: true })).toBeVisible() await expect(page.getByText('Engineer', { exact: true })).toBeVisible() From f4104c50cb940679ea4e3a5d7f0e76ea2bcca339 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Sun, 31 May 2026 01:01:36 -0500 Subject: [PATCH 13/13] test(e2e): assert cell positions via aria-label, not text content --- tests/calc.spec.ts | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/tests/calc.spec.ts b/tests/calc.spec.ts index 6d23b4b..517b27d 100644 --- a/tests/calc.spec.ts +++ b/tests/calc.spec.ts @@ -28,13 +28,16 @@ test.describe('Calc', () => { // 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. - await expect(page.getByText('Name', { exact: true }).first()).toBeVisible({ + // 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.getByText('Role', { exact: true }).first()).toBeVisible({ + await expect(page.getByLabel('Cell B1', { exact: true })).toHaveText('Role', { timeout: 15_000, }) - await expect(page.getByText('Score', { exact: true }).first()).toBeVisible({ + await expect(page.getByLabel('Cell C1', { exact: true })).toHaveText('Score', { timeout: 15_000, }) @@ -45,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()