DEV Community

Cover image for Building an in-browser Excel formula grader with Univer and Astro
MarsBuildLog
MarsBuildLog

Posted on Originally published at sheetdo.com

Building an in-browser Excel formula grader with Univer and Astro

Most coding judges run code in a backend sandbox. For Excel practice, that model is a bad fit: you want real spreadsheet feel, instant feedback, and zero compute cost per submit.

On SheetDo, we grade formulas entirely in the browser: Astro SSG for SEO-friendly pages, Univer for the grid + formula engine, and a small React island that runs multi-case checks locally.


Why Astro + Univer

Three goals drove the stack:

  1. SEO + fast first paint — practice pages need crawlable HTML and structured data.
  2. Real Excel interaction — selection, formula bar, cell refs, Canvas grid.
  3. Zero backend compute — every recalculation stays on the client.

We also skipped writing our own formula parser. Real Excel needs ranges, absolute refs, dynamic arrays, and messy type coercion. Univer’s @univerjs/engine-formula already does that.

Astro static shell (SEO, copy, JSON-LD, skeleton)
        │  client:only="react"
        ▼
ExerciseRunner (React island)
        │
        ├── SheetEditor (Univer grid + formula engine)
        └── Multi-case grader (inject data → recalc → compare)
Enter fullscreen mode Exit fullscreen mode

Pitfall 1: Univer and SSR don’t mix

Univer needs window, document, and Canvas. Astro SSR will crash it. We mount the runner with client:only="react" and ship a matching Astro skeleton as slot="fallback" so the layout doesn’t jump while the island boots.

<ExerciseRunner client:only="react" exercise={exercise} locale={locale}>
  <ExerciseRunnerSkeleton slot="fallback" exercise={exercise} locale={locale} />
</ExerciseRunner>
Enter fullscreen mode Exit fullscreen mode

Vite also needs help. Univer’s package graph is deep; stale optimizeDeps caches caused blank islands in dev. We pin the full import set in astro.config.ts and mark @univerjs/* as ssr.noExternal.


Pitfall 2: lock the sheet, but still mutate for grading

Learners see a full spreadsheet. Without locks, they can edit source data and “pass” by cheating the inputs.

We cancel edits outside the answer cells with Univer events (BeforeSheetEditStart, BeforeCommandExecute), then open a short programmatic escape hatch for grading:

async function withEditableSheet(mutate) {
  allowProgrammaticMutateRef.current = true;
  try {
    return await mutate(sheet);
  } finally {
    allowProgrammaticMutateRef.current = false;
  }
}
Enter fullscreen mode Exit fullscreen mode

Users can’t touch locked cells. The grader still can rewrite cases and apply formulas.


The grader: multi-case, anti-hardcoding

A single expected value isn’t enough. =1500 would pass a sum exercise. On Submit we:

  1. Require a formula starting with =.
  2. Optionally require a function keyword (XLOOKUP, SUM, …).
  3. For each test case: inject cells → apply the user’s formula → wait for calculation → compare.
  4. Restore the visible case and show pass/fail per case.
for (const tc of testCases) {
  await editor.updateCells({ ...baseCells, ...tc.cells });
  const value = await editor.applyFormula(target, formula);
  evaluations.push({ actualValue: value, expectedValue: tc.expectedValue });
}
Enter fullscreen mode Exit fullscreen mode

The subtle bit: Univer calculates asynchronously. After setFormula, wait for onCalculationResultApplied before reading the cell — otherwise you grade a stale value.

Comparison also tolerates floats, currency symbols, and commas (valuesEqual with a 1e-9 epsilon).


Headless CI for the exercise bank

Every JSON exercise has official solutions. We boot a UI-less Univer in Vitest (core + sheets + formula plugins only), run each solution against every case, and fail CI if an answer drifts. No Playwright, no browser — seconds for the whole bank.


What we got

Backend sandbox Client grader (Astro + Univer)
Latency hundreds of ms+ tens of ms
Compute cost scales with submits ~CDN only
Offline weak works after first load
SEO SPA friction static HTML

Takeaways: keep heavy spreadsheet UI out of SSR; grade with injected cases, not string matching; always wait for the formula engine before reading results.

Live at sheetdo.com.

Top comments (0)