Skip to content

Verifying an app

12 min read · Updated 3 Nov 2026

This page belongs to Data App developer guide. Inspect the rendered dashboard and measure layout geometry at the target viewport sizes. Check settings explicitly: a missing option can look like an intentional default, such as stacked bars where grouped bars were requested.

Verify question coverage and semantic capabilities

Section titled “Verify question coverage and semantic capabilities”

Match every required analysis to a useful saved Business Question and compare the displayed result with its complete definition and equivalent live execution. Confirm missing questions were actually saved and any required entities are available to the query engine.

Verify the capabilities selected through the App build checklist: period calculations, group membership, other semantic features and the chart behavior that presents them. Retain expected and observed results. Record each inapplicable, unsupported or blocked item with its reason. A successful compile cannot establish analytical correctness or satisfy a missing required feature. Recheck affected capabilities when extending or restyling an App.

Match the implemented views and controls to the brief. For a supplied reference, check the panel inventory item by item. Report omissions and deliberate adaptations. Compare representative displayed values with equivalent live executions using the same definitions, grain, filters, parameters and access context. Recheck evidence when the model or source data changes; an earlier sample is not a fixed total.

Reconcile totals and breakdowns only for additive measures over the same complete population. Distinct counts over overlapping groups, averages, rates and ranked subsets need checks appropriate to their definitions. Account for rounding and incomplete result pages. Label partial periods before comparing them with whole ones; every headline figure must come from its governed measure.

Check configuration and rendered behaviour

Section titled “Check configuration and rendered behaviour”

Check compilation issues when using definitions. When using operations, validate their payloads and check each result. Configuration checks can run without mounting a chart, but the current package requires a browser.

After mounting, verify the requested settings and interactions. Compare vSpecs when investigating a setting that did not apply; see Check operation results.

A selection badge proves selection state, not query filtering. Apply one category whose expected result differs from the full dataset and compare every affected view, including a measure-only KPI and the campaign or detail table. Check the actual values and returned groups after loading settles. Discard must preserve applied results; Clear must restore the verified unfiltered result. Repeat this check when changing the SDK or visualization package combination. A failed filter check prevents a working-demo claim even when compilation and the unfiltered totals pass. Check linked controls and defaults together. Verify query success as well as changed values; a failed query may leave old values visible. When changing date filters, check comparison periods remain in scope. Refresh objects in place, following Interactivity for the supported selection behavior.

Inspect KPI typography at the actual target widths. A large card can contain a small default number; use the number visualization’s supported font-size operation and check its result rather than styling internal chart DOM.

Screenshots may be scaled. Read element dimensions directly when checking sizes and proportions:

const size = (selector) => {
const box = document.querySelector(selector).getBoundingClientRect();
return { w: Math.round(box.width), h: Math.round(box.height) };
};
({
frame: size('.chart-frame'),
card: size('.kpi'),
scrolls: document.documentElement.scrollHeight > innerHeight,
});

Check a wide desktop, a laptop, each breakpoint, the narrowest supported width and a short window. Compare proportions at multiple sizes.

For an overview dashboard or when one object fills the window, scrolls must be false at every size above the layout’s scrolling threshold. When sections stack, each frame that scrolls inside must be clearly shorter than the window. See Choose what scrolls.

For a row of controls, read each control’s getBoundingClientRect().top; they should be the same number.

For a mixed overview, check the following after data and layout have settled:

  • No horizontal page overflow, and no vertical page overflow above the declared thresholds. Confirm that every visible card remains inside the board; hiding page overflow does not pass.
  • Each chart host has a positive width and height and fills the card’s content area after its title, subtitle, note and padding. Inspect unintended blank bands visually; whitespace inside the plot may be necessary for axes or labels.
  • Related cards have aligned edges and consistent gaps at each layout tier.
  • Compare a short table result with a long one using the same viewport. The table frame and neighbouring charts retain their dimensions within a 2px rounding tolerance; additional rows remain reachable through the table’s own scrolling or pagination.
  • Filtering to empty results, opening controls and switching detail tabs do not unexpectedly change the height budget. Check the deliberate stacked layout separately.

For a before/after comparison, keep the business brief, filters, package versions, viewport and theme the same. Use the same model revision and data snapshot where possible; otherwise record changes and compare geometry separately from values. Save screenshots and measured geometry for both results; record query failures separately from layout failures.

Test the shortest and longest content, missing values and other relevant variations. Where practical, test every record; when two records appear together, test pairs that stress the layout. Wait for data and rendering before measuring. For example, use an app-specific helper that selects a record and waits for its render to complete:

const problems = [];
for (const id of recordIds) {
await selectRecordAndWaitForRender(id);
if (document.documentElement.scrollWidth > innerWidth) problems.push(`${id}: horizontal overflow`);
if (Math.abs(figureBottom() - floorTop()) > 2) problems.push(`${id}: figure off its floor`);
}
problems;

Report how many cases and sizes you checked.

Check text overflow at each target viewport size:

[...document.querySelectorAll('h1, h2, h3, .kpi-label, .kpi-value, .chart-title, button')]
.filter((element) => element.scrollWidth > element.clientWidth + 1)
.map((element) => [element.textContent.trim(), element.scrollWidth, element.clientWidth]);

Inspect the returned elements for unintended overflow or clipping.

Check table cells separately for ellipsis and clipping, and adjust column widths where needed; see Tables.

Check the page itself does not scroll sideways at every viewport you support, including 1280 and 1024 px wide:

[document.documentElement.scrollWidth - innerWidth, window.scrollX];

Both must be 0. A canvas drawn past its panel or a grid column sized auto to no-wrap content can push the page wider than the window, and a screenshot then shows the header clipped on the left. Give the shell grid-template-columns: minmax(0, 1fr) and clip horizontal overflow on chart surfaces and panels.

A page can render without an error and still say something false. Read it as its reader would, on every change, with the words as the evidence:

  • A label must be true of the state on screen. “Selected dates” with nothing selected, or “in thousands” over values in millions, is a defect.
  • A sentence on the page is a claim. Check each one against what the page shows: a caption saying “highest first” over rows that are not is false however well the table renders. Make it true or change it, and write it once the behaviour works rather than ahead of it.
  • A word that names a direction, a rank or a best or worst, such as “gain”, “decline”, “leads” or “strongest”, is a claim about the current selection. It must stay true when every value falls, when one item remains and when nothing is selected, so derive it as Narrative describes or word it neutrally, such as “Largest change”. Being first in a ranking does not make a change a gain. Check direction words with a selection that reverses the sign, and rank or best and worst words with one that changes the order.
  • Finish on the page as it opens. Clear any selection you tested with and inspect that state, because it is the one every reader sees first.

One range that looks right does not establish a comparison. Each time you add or change one, check:

  1. the state the page opens on, with nothing selected;
  2. a range you chose;
  3. a period with no earlier data, such as the first in the dataset, and one whose earlier period the data covers only in part.

In each state, read both sides beside the change, as What a comparison is about shows. The current side must equal the figure beside it, the prior side must be complete, and the label must name what is compared. Hold the result against the page too: a large change over a flat trend is broken until you can explain it. Where a state has no complete prior period, the slot must be empty or say why; see Show a change only when it means something.

The same concept shown in two places must have the same value, or each label must say why it differs. For every figure that appears more than once (a KPI card and a headline, a summary and a detail tab, a table total and a chart), compare the rendered values. Common causes of a mismatch:

Symptom on screen Cause
NRR 114.7% in the summary, 104.2% on the growth tab One query covered all months, the other the latest month.
A headline ratio that differs from the chart beside it The headline queried a different period, or ignored the selection the chart follows.
Expected loss larger than ARR at risk The two measures cover different populations, or one is SUM(x) × AVG(p) instead of the row-level SUM(x × p).
Gross-profit impact equal to revenue impact The same expression was used for both.
A scenario that differs from the governed value with “no change” The scenario model does not reproduce the governed measure at its default inputs.
A winner shown behind the leader The measure aggregates over the whole event instead of reading its final point.
A headline that disagrees with its own KPI card A cache or “latest request wins” rule let one query’s result replace another’s.

Label every figure with its period, and state the selection scope where it is not the page’s. See also Check the measure at every grain.

For blank charts or readiness timeouts, check page visibility and animation-frame activity:

document.visibilityState;
let frames = 0;
const start = performance.now();
const tick = () => {
frames++;
if (performance.now() - start < 1000) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
// read `frames` after a second

Browsers can pause animation frames in hidden tabs. Successful queries and populated DOM nodes do not establish that a chart rendered; check it again with the page visible. Follow the readiness guidance when handling a timeout from a background tab.

Check for a zero-width viewport (innerWidth === 0) and zero-size chart hosts.

Inspect chart marks and labels visually after rendering settles; DOM text alone may not include labels drawn by the chart. Check units, date and category labels, clipping and intentional values printed on marks. Verify that category and state colours keep their meaning across related views and filtered results. A static unfiltered screenshot does not establish those mappings.

  • Initial queries. Measure from page navigation until the visible objects display data, including preparation before mounting. Check that defaults are applied to the first query, with no extra unfiltered query before them. Distinguish duplicate initial queries from metadata, retries, pagination or additional queries required by the visualization. Follow Prepare the first query.
  • Hidden-panel queries. Count query requests in the network log, or with performance.getEntriesByType('resource'), before and after switching panels, and count the hidden host’s children.
  • Interaction cost. Count and time the queries caused by a row click, a filter change and each tab switch, and the time until the last one finishes. A tab switch with no selection change should send none, unless the tab was hidden during an earlier change and syncs its stale selection state when opened. See Measure load and interactions.
  • Closed dialogs. Read getComputedStyle(dialog).display, and check what document.elementFromPoint() returns at the centre of the page.
  • Empty regions. textContent includes hidden children. Read hidden or the computed display of each element instead.
  • Query responses. Inspect response bodies in the network log.
  • Generated files. Validate exports with a compatible reader; see Excel.
  • Verify in a visible, foreground tab. A hidden or background tab (document.hidden) does not paint charts, so marks cannot be clicked and screenshots show empty plots. A minimised or covered browser window reports hidden too, and an app that waits for animation frames may never finish loading there. A headless browser renders regardless of window state and is the reliable way to screenshot many pages.

  • Screenshot only after every visible object has data. Count a table as ready only when it has body rows, not just a header, and wait until the count has been stable for a few seconds.

  • Dispatch synthetic pointer events at coordinates computed from getBoundingClientRect(), rather than guessing pixels from a screenshot.

  • Dispatch on an element, never on window.

  • Prefer geometry to event.target where an overlay can cover the element under the pointer.

  • A test that ends in a click or pointerup changes the page. Reset it before the next check.

  • Inspect the rendered result after running assertions.

When the page never gets past its loading state, and there is no failed request and no log line:

  1. Measure preparation time. Time a cold development startup before concluding that it has stalled; compiling definitions and applying a large table’s operations can take many seconds. Cache repeated preparation by definition and operation list.

  2. A stale development dependency cache. With Vite, stop the dev server, remove node_modules/.vite and restart. Only do this when no other dev server is using the same project directory, because they share that cache.

  3. Two dev servers sharing one cache. Projects that share a node_modules also share its Vite cache, and two servers running at once overwrite each other’s optimized dependencies. Give each app its own cache directory:

    vite.config.ts
    export default defineConfig({ cacheDir: '.vite' });
  4. A chart mounted into a zero-height container. Look for the zero-size warning; see Give every host a definite box.

Log before and after each await, then reload to identify which promise remains pending. Check timings before removing inputs, since reducing the workload can hide slow preparation.

  • On every interactive page, select a value from a filter and from a chart click: a chip appears in the selection bar. With automatic apply, wait for the apply debounce and confirm that the figures change; with manual apply, click Apply first. Remove the selection with its chip control and with Clear all: the chip goes and, after the debounce or Apply, the figures return.
  • For lazy-mounted stories and tabbed charts, measure initial load and the first reveal of a section. Check that inactive hosts are unmounted, applied selections appear when a section returns, and rapid scrolling or tab switching does not leave duplicate mounts, stale values or empty charts. Follow the panel lifecycle.
  • Run the app’s production build.
  • Reset any viewport emulation you used.
  • Check both themes, and a non-English browser locale.
  • For an overview or where one table or list fills the window, check the page itself does not scroll above its declared thresholds; where sections stack, check every inner scroller is shorter than the window.
  • Say plainly what you verified and what you did not.