Skip to content

Timeline 2.0 - #157

Open
passsy wants to merge 59 commits into
mainfrom
dart-3.8-jaspr-latest
Open

passsy wants to merge 59 commits into
mainfrom
dart-3.8-jaspr-latest

Conversation

@passsy

@passsy passsy commented Aug 5, 2026 •

Copy link
Copy Markdown
Owner
Screen-Capture-2026-08-06-01-05-41.mp4

The generated index.html was a vertical list of screenshots with the event details underneath. It is now a workbench: a filmstrip of frames along the top, and below it the capture, the widget tree, the source that triggered the event, and the raw payload.

Reading a test

  • Filmstrip of every recorded frame, one capture each, with the events of a frame stacked in its column.
  • Widget tree with search, offstage badges, and the selected widget outlined on the capture. Only the rows in view are rendered, so a tree of several thousand nodes no longer blocks the page for seconds.
  • Source of the line that triggered the event, scrolled to the caller with a few lines of lead-in.
  • Lightbox on the capture, stepping through captures with ←/→ and through a frame's events with ↑/↓.
  • Keyboard: ←/→ frames, ↑/↓ events, Space opens the capture, Home/End jump to the ends. The widget tree takes the arrows for itself once clicked into.
  • A failed test now ends with a Test Failed event carrying the real expect message and the caller line, and the report opens on it. flutter_test replaces the exception with a placeholder before it reaches us, so the real one is caught through FlutterError.onError. An error the test took with takeException() is not mistaken for the failure. Its screenshot is only used when the failing frame is still on screen — a teardown that pumps discards it rather than showing something else under a failure heading.

Frame accounting

A test's frame count is the metric this was missing: 30 frames drawn used to move the old counter by 1, because it only counted frames an event was recorded in.

renderedFrameCount now counts every frame. The strip shows the frames it has information for and collapses the rest into a gap — a narrow column saying how many frames went by with nothing recorded. Arrow navigation steps over them.

Hovering a frame gives three figures, all measured on the frame itself:

Generation real time the framework spent on build, layout and paint
Test work real time the test spent after it, on assertions and captures
Clock step how far a pump(duration) moved the simulated clock

The engine reports nothing to a widget test — addTimingsCallback never fires, because the automated binding does not rasterise. Instead the frame is bracketed: a transient callback in handleBeginFrame, before build/layout/paint, and the persistent callback spot already installs in handleDrawFrame, after them.

The clock is deliberately not one of the costs. fakeAsync holds it still during real work, so simulated time can only report what a pump asked for, never what anything took.

Platform

  • The floor moves from Dart 3.4 / Flutter 3.22 to Dart 3.8 / Flutter 3.32, which Jaspr 0.23 requires (up from 0.17).
  • Dart 3.8 switches dart format to the tall style and turns on two lints, so about 80 files that have nothing to do with the timeline are reformatted. Those are the last two commits and can be skipped in review.
  • The compiled client grows from 151 KB to 237 KB with the new UI.
  • tool/serve_timeline.dart serves a generated report over loopback, since the inspector only renders client-side.

Worth a closer look

_startCountingRenderedFrames in lib/src/timeline/timeline.dart is the highest-risk change here, because it hooks the scheduler in every test that touches the timeline. Three constraints shape it, and each one was a failing test first:

  • scheduleNewFrame: false — the usual scheduleFrameCallback requests a frame, which leaves one permanently scheduled and stops pumpAndSettle ever settling.
  • The transient callback is dropped as soon as the scheduler goes idle. flutter_test fails a test that leaves one behind, and checks straight after the test body, long before any teardown.
  • Frames are only measured under AutomatedTestWidgetsFlutterBinding. A live binding draws from a real vsync, so nothing at the end of a frame says whether another is coming, and the idle heuristic does not hold.

Report directories. A run that writes no report now deletes the previous one, so a stale report cannot be mistaken for the current run's. Reports are keyed by test name, so two identically named tests in different files share a directory. A report records which test file wrote it and only that file deletes it again, so one of them passing no longer removes the other's failure report. Both failing still overwrite each other — spotted, not fixed.

lib/src/timeline/html/sources/script.js.g.dart is the compiled client, checked in as a 237 KB string and shipped to pub.dev. Regenerate with dart run tool/compile_js.dart after touching anything under html/. It is not dart format clean and should be left as the generator writes it.

passsy added 30 commits August 6, 2026 01:37
Add frame-based navigation, resizable capture and inspector panes, and searchable widget inspection while keeping reports usable through file URLs. Compress and deduplicate frame data so large reports stay responsive instead of exhausting the browser renderer.
The details pane put the event's metadata in a sidebar table, which cost
the pane half its width to say six things, and left the source excerpt a
narrow column beside it. The pane is one column now and the table is
gone:

- The heading is the event type, with the dot in the event's own colour
  that the timeline markers already use. A failure gets a red halo, so
  the reason the report is open is the first thing on screen.
- Both clocks are one line under the event text. A widget test runs
  inside fakeAsync, so `timestamp` is simulated time that jumps by
  whatever a pump was given. That answers what the test asked for but
  not how long anything took, which is why TimelineEvent now also
  records wallTime and the report shows both, always.
- Caller is gone. The source section names the same file and line.
- Capture is gone from the table. The capture in the pane on the left
  opens full screen instead, dismissed by the backdrop, the close
  button, or Escape. Overlays are absolutely positioned over the same
  box as the image, so annotations stay aligned however it letterboxes.

The source section shows the whole file rather than six lines either
side of the caller. Any fixed window cuts off the part of the method
that explains the event. Files are stored once per path and referenced
by events, otherwise a report would carry the same file once per event.
The pane scrolls the caller line to the middle on selection, since the
file no longer starts at the interesting part.

Reports open on an event instead of "Select an event": the failure when
there was one, the first event otherwise. Failures are marked by the
producer through a new isFailure flag rather than the report matching on
the label text.

Source locations open in VS Code as well as IntelliJ. Only one IDE runs
a test, so jetBrainsLink becomes ideLink plus ideName, which also turns
the hardcoded "IDEA" button label into data. The URLs move into
ide_link.dart behind a function taking the environment and the working
directory, which is what makes them testable at all -- there was no test
for the IntelliJ link before, there are eleven for both now.
A captured tree runs to tens of thousands of nodes and each row is
around seven DOM elements, so building all of them blocked the page:
measured at 955 ms for 5461 nodes, and it grows with the tree. That is
the freeze behind "Expand all", and behind typing in the search box,
which expands every node on the way to a match. The Tree text tab felt
instant next to it because it only ever renders 250 lines.

The tree is now flattened into a list of rows and only the rows in the
viewport are rendered, with a spacer above and below so the scrollbar
still measures the whole tree. Every row is the same height, which is
what makes the height of an unrendered row knowable. At 16381 nodes,
fully expanded: 58 rows and 407 elements in the DOM, no long task at
all, and a search matching 9829 nodes stays just as quiet.

That removes the reason the tree was only partly expanded, so the
"large tree" mode and its notice are gone and everything is expanded
from the start.

A chain of single-child widgets now stays at one indentation, the way
Flutter's own inspector shows it. Widget trees are mostly such chains
and indenting every one of them pushed the interesting rows off the
right edge: an 18-deep test tree indents 6 steps instead of 18.

Enter in the search box scrolls to the match itself rather than calling
scrollIntoView on an element that may not be rendered, and scrolls
sideways when the match sits past the right edge.

The lightbox can toggle the overlays. It shares the capture pane's
switch, so the annotations do not disagree between the two.
The pane holds the whole file, so it opened at line 1 with the assertion
somewhere below the fold. It was meant to scroll already, but did so
against whatever the DOM held at the time, which was wrong twice:

- On load the assignment landed before the pane had been laid out, and
  setting scrollTop on something with nothing to scroll does nothing.
  Then hydration replaced the pane and took the position with it.
- Switching events scrolled to the previously selected event's line,
  because rendering happens after the state change. It looked right only
  while both lines happened to sit inside the same clamped view.

The caller line now carries its line number, so the scroll waits until
the pane actually holds the line it was asked for, waits until there is
something to scroll, and holds the position for three frames so
hydration cannot undo it. After that it stops, leaving a reader who
scrolls away where they left off.
The scroll landed a constant distance too far down: caller line 52 put
line 76 at the top, line 57 put line 81 there. offsetTop is measured
from the closest positioned ancestor, and the source pane is not
positioned, so it carried the pane's own distance down the page into
the target.

Measuring the line and the pane with getBoundingClientRect and adding
the current scrollTop makes the target relative to the pane whatever
sits above it. Line 52 now puts line 48 at the top.

Also stops centring the line. Four lines of lead-in leaves the caller
near the top, where it is easier to find and where more of the code
after it is on screen.
Carrying the event row across frames dropped you into the middle of the
next frame's story, hiding the assertions that led there. Left/right now
always select the frame's first event, which is what clicking a capture
in the filmstrip already did.
Everything it showed exists elsewhere: the toolbar already carries the
frame and event arrows, the summary bar already reads 'Frame 1 - Event 1
of 8', and the details panel already heads with the coloured event type.
The inspector is now a single row, so the workbench gets that height back.
Only spot's own assertions reported their failure through addEvent. A
plain expect, or anything else the test threw, never reached the
timeline, so the report ended at the last thing that worked and never
showed why the test failed.

The failure now gets a frame of its own at the end, which is also what
the HTML report opens on. Reading it back from the test is not possible:
by teardown, test.errors holds only 'Test failed. See exception logs
above.' with an empty stack trace, because flutter_test dumps the real
exception to the console and reports that placeholder in its place. So
FlutterError.onError is wrapped for the duration of the test, which is
where the real exception and its stack still pass through.

The stack in the event drops the framework frames. A failure inside
Flutter is reached through a dozen frames of gesture arenas, fake async
and the test runner, and every async gap repeats them; what is left is
the path through the test and the app. The console still gets the full
trace.
Up and down walk the rows on screen, left and right close and open the
selected node. The tree claims the arrows on a click into it and gives
them back on the next click anywhere else, marked by a ring around the
list so the mode is visible. Home and End keep jumping between events
either way.

Selecting a node already needed the flattened rows, and scrolling to one
recomputed them a second time, so both now go through _currentTreeRows().
Only _select scrolled the strip, and initState sets the selection
directly, so a report that opened on the failure showed it selected while
the strip still sat at frame 1 with the failure off the right edge.

The scroll itself was also scheduled for the next microtask, which on the
first load runs before the strip has been laid out, and hydration takes
the position with it shortly after. It now waits for the strip to be
scrollable and holds the position for a few frames, the same way the
source pane does.

Replaces scrollIntoView() with the least scroll that puts the event on
screen, so a nearby event no longer drags the strip across, and does it
for the frame lane too, which scrolls on its own when a frame holds more
events than fit.
The capture was taken during spot's teardown, which runs after the test's
own teardowns because it is registered first and teardowns run last in
first out. A teardown that pumps a frame therefore drew over the failure
before it was captured, and the report showed that as the failing frame.

The tree is now snapshotted the moment FlutterError.onError sees the
error. Snapshots are only valid for the frame they were taken in, so
getting the same object back at teardown proves nothing has been drawn
since and the capture really is the failing frame. When something has,
the event goes without a capture rather than with a misleading one.

The event's widget tree comes from that same snapshot for the same
reason.
The timeline only ever numbered the frames it recorded something in, so a
test that rendered 5000 frames and asserted 11 times reported 11 frames.
That hid the one number that says what a test costs.

SchedulerBinding keeps no frame counter and persistent frame callbacks
cannot be removed, so one callback counts for the whole process and each
timeline takes a delta off it. Frames now carry their real number, the
summary shows the total, and the stretch between two recorded frames is
drawn as a gap with the frames that went by and how long they took on
the test clock and the wall clock.

A gap holds nothing to select, so frame navigation walks recorded frames
and steps over them. The three rows of the strip share one set of grid
tracks, which is what keeps them lined up now that columns differ in
width.
printHTML deletes and rewrites the report directory, so a run that
reports keeps it current. A run that does not report left it untouched:
fix the test, run it again, and the link from the failing run still
opened the old report, showing the source as it was before the edit
along with events and captures from a run that no longer existed.

Nothing was stale in how a report is built, the report simply was not
the one the last run produced. reportOnError on a passing test and the
off mode now remove what an earlier run left behind, so the link is
plainly dead instead of quietly wrong.

The directory is derived in reportDirectory() now, shared by writing and
deleting, which is also what the hot-restart link uses.
Three ways to get a widget through the same 900ms animation, and the
frame count each one leaves behind: pumpAndSettle at its default 100ms
step, at a 16ms step, and a single pump straight to the end.
The gap was a five line block as wide as a frame, which pushed the
frames it sits between apart for information nobody reads at a glance.
It is now 24px of hatching with the count running vertically, and the
timings moved into the tooltip.

The animation example takes its expectations from the duration now, so
changing the duration does not mean rewriting numbers by hand.
The native title tooltip took about a second to appear and cannot be
styled. This is the same information in a small card, shown after 300ms
of hover so brushing past a gap does not flash it, and hidden again
straight away.

CSS only, so it works before the page hydrates. A gap always sits
between two frame columns, so the card is wider than the gap without
risking a clip at either end of the strip.
The ruler cell had room for one of each and showed the test clock and
the frame number. Both numbers have a counterpart worth seeing next to
them: the wall clock says what the step really cost, and the spot count
says where the column sits in the strip while the Flutter count says how
far the test had rendered by then. Clocks on the left, counts on the
right, two rows.

The ruler grew 13px, which pushed the event lane out of the panel, so
the panel opens 14px taller.
The letter S in an orange box was a placeholder. The magnifier is drawn
inline as a data URI so the report stays a single self-contained file,
and it still reads at 16px.

The event lane only had room for two rows, which hid most of what a busy
frame recorded. The panel opens at 320px now, which is five rows, and it
is still draggable from there.
The help cursor swapped the pointer for a question mark on the way past,
which is a lot of movement for a card that appears on its own anyway.
All four arrows move between captures, skipping frames that have none:
there is nothing to put on screen for those, and landing on an empty
lightbox reads as the report having lost the image. The ends hold rather
than wrap, the way frame navigation does.

The lightbox now takes the whole keyboard while it is open, so the
timeline no longer moves underneath it. The selection follows the
capture instead, which is where the report is left when it closes.

The caption gained the position, because stepping needs something to
orient against and a clamp at either end is otherwise silent.
Left and right walk the captures, up and down stay in the frame and walk
its events. Every event of a frame shares the one capture, so what
changes is the overlay drawn over it, which is the point: which widget
each assertion in that frame matched.

The caption says which event of the frame is showing, and the actions
bar names both pairs of keys, since they are the only way to reach any
of this.
Space opens the selected frame's capture full screen, and closes it
again so the key is not dead once the lightbox is up. Nothing happens on
a frame that was never captured.

The lightbox no longer explains its keys. The toolbar already lists the
others and now lists this one too, which is where a reader looks for
them anyway.
A big widget tree is rendered one page at a time so the DOM never holds
thousands of lines. Pins that a page carries only its own lines and knows
whether there is more in either direction.
The timeline HTML referenced screenshot and overlay files without
anything checking they had been written. Reads the URLs back out of
events.json and looks for the files on disk.
The source pane capped at max-height 360px and .details-content was a plain
block, so nothing handed it the leftover height. Dragging the divider up made
the inspector taller and left the file view the same size.

The chain runs on definite heights now, and .source-panel keeps a 220px floor
so a short inspector still shows a readable slice of the file.
Modal, Events and ExpandableBox were the timeline UI before the workbench
replaced it, and have been unreachable since. ModalState also subscribed to
window.onKeyDown in initState and never cancelled it.
The StyleRule chain from SnackBarState up through TimelineAppState and the
server app into Document(styles:) existed to carry seventeen lines of snackbar
CSS, and emitted a second <style> for them. Rules landed in whichever of the two
stylesheets the last person to touch them happened to open.

The snackbar rules and its keyframes move into timelineCSS, the three
forwarders go, and with them the theme constants the deleted components were
the last users of.

The comment justifying raw CSS claimed css() could not express media queries or
focus states. It can, and keyframes too. The real reason is size, so it says
that now.

Also cancels the snackbar's dismiss timer, which outlived the component and
called setState on it.
Jaspr moved to package:universal_web itself and its JS interop guide says to
use it for browser APIs in server and static mode. dart:html is deprecated
since Dart 3.7 and cannot compile to Wasm.

The hand written web_stubs.dart declared 'external dynamic window', so every
DOM call in the report was dynamic, guarded by ignore comments and propped up
with casts like (event.client.y as num). universal_web is package:web in the
browser and stubs on the VM, which is the same conditional import done by a
package that also brings the types along. It was already a transitive
dependency through Jaspr.

Casts between interop types are unchecked, so the places that read properties
off an event target now test with isA before casting rather than trusting the
cast to fail on the wrong thing.

Window has no onMouseDown/Move/Up getters the way Element does, so those go
through EventStreamProviders, which is what the Element getters are built on.

Side effect: the compiled bundle drops from 304,021 to 232,654 bytes, because
package:web is extension types where dart:html is a library.

Renames TimelineServerApp and TimelineClientApp along the way. Jaspr exports
ServerApp and ClientApp, the latter being its hydration entry point, so
runApp(const ClientApp()) read as the framework call and was not, and
render_timeline had to import jaspr/server.dart with 'hide ServerApp'.
passsy added 10 commits August 6, 2026 01:37
The values are elapsed since the first event of the test, but a leading + reads
as a delta from the column next to it. Nothing else in the strip is relative to
its neighbour.

The unit stays. These are durations, and the scale switches at one second, so
bare numbers would put a 590 and a 1.59 side by side meaning 0.59 s and
1.59 s.
The gaps already reported their two clocks on hover, the frames between them
did not. Same card, same measurement, so a frame and the gap beside it read as
one sequence.

The span runs from the last event of the previous frame to the first event of
this one. When a gap sits in between it covers the gap too, and the note says
so: the numbers are the step to the frame, not the paint of it.

The card is anchored to the left edge of a capture rather than centred on it.
Centred, the first frame's card reached past the left edge of the strip, and a
scroll container cannot scroll to reveal that. The gap card stays centred,
since a gap never sits first.
frameStepCost and gapBetween were the same computation, so a gap and the frame
after it always showed identical numbers. The same milliseconds appeared twice,
once as a stretch of dead frames and once as the cost of one frame.

Nothing records where the unrecorded frames end and the next one begins, so
that span cannot be split. It belongs to the gap, which already reports it. A
frame after a gap now says it is timed with the gap instead of repeating it,
and only a frame sitting directly after another reports a cost.

The test asserting the two agreed was asserting the bug.
The engine reports nothing to a widget test: addTimingsCallback never fires,
because the automated binding does not rasterise. But the framework brackets
every frame and both ends are reachable. A transient callback runs in
handleBeginFrame, before build, layout and paint; the persistent callback spot
already installs runs in handleDrawFrame, after them. Between the two is the
framework generating a frame, between one frame and the next is the test.

So each frame now carries three figures of its own: what the framework spent
generating it, what the test spent afterwards on assertions and captures, and
how far a pump moved the simulated clock. Measured on the frame, not derived
from the distance to the neighbouring one, so a frame reports the same whether
or not unrecorded frames sit in front of it. A gap reports the first two summed
over the frames it covers.

The clock is not one of the three costs. fakeAsync holds it still during real
work, so simulated time can only say what a pump asked for.

Two things keep the transient callback from breaking tests. scheduleNewFrame
false stops it requesting a frame, which would leave one permanently scheduled
and stop pumpAndSettle settling. And it is dropped as soon as the scheduler
goes idle, because flutter_test fails a test that leaves a transient callback
behind and checks straight after the test body, long before any teardown.
Under a live binding nothing at the end of a frame says whether another is
coming, so frames are only measured under the automated one.
A capture taller than its filmstrip tile stretched the row it sat in,
pushing the caption out of the tile and drawing the image over the event
lane below it. The image sized the row because the row was auto and the
image's percentage height fell back to its own aspect ratio.

The filmstrip row is now definite and the thumbnail is out of flow, so
`object-fit` has a box to fit into and the capture letterboxes, centred,
whatever its shape.

The capture pane had the same shape of bug hidden behind a magic number:
the image was capped at `100vh` minus the header, the timeline and 160px,
which ignored the pane's real height. On a short window it shrank the
capture to a sliver; the number could go negative. The canvas now carries
the capture's own ratio and fits itself to the pane, which also makes it
exactly the image box the widget outline is positioned against.
A tile was a fixed 132x111 box whatever the test rendered into it, so
anything but the default 4:3 surface sat in it letterboxed, with dark
bars for the rest of the tile.

The tile now takes the aspect ratio of the captures, which for most tests
means it is exactly the capture. A test is free to resize the surface
halfway through, so the strip takes the shape most of its captures have
and the odd frame of another shape letterboxes in its tile. Nothing is
cropped either way.

Both ends are bounded, because a test can render at any size: a very wide
capture must not squash the tile to a line, and a very tall one must not
push the ruler and the event lane out of the panel. The upper bound is
what the panel has left over, so dragging the panel taller grows the
captures until they reach their own shape.

The captures' size now travels on the event rather than inside its gzipped
widget tree, which the strip would otherwise have to decompress for every
frame to learn.
It is a label on the strip, not content. Dragging across the filmstrip
picked up "188 frames" along the way.
The tile clipped its own overflow, which the hover card is. It never
needed to: the filmstrip row is definite and the thumbnail is clipped
where it sits, so nothing else reaches past the tile.
@passsy
passsy force-pushed the dart-3.8-jaspr-latest branch from e44f771 to b2ade3f Compare August 5, 2026 23:39
The timeline installed a persistent frame callback of its own to count
frames, which is what FrameClock now does for the whole process. Two
callbacks were counting the same frames.

Only the cost measurement stays here: FrameClock says how many frames
went by, the bracket around a frame says what each one cost to generate
and how much time the test spent between them.
@passsy passsy changed the title Rework the timeline report into an interactive inspector Timeline 2.0 Aug 6, 2026
passsy added 17 commits August 11, 2026 05:22
Conflicts:

- `screenshot.dart` — main's per-frame raster cache replaces the direct
  `toImageSync`, and the branch's capture render object rides along.
- `CHANGELOG.md` — main restructured 0.20.0 into sections. The branch's
  entries move into a Timeline section, and the `act.inspectTap()` entry
  main already carries is no longer listed twice.

Main caches what a widget fills into `debugFillProperties`, and the
branch's tree snapshot filled them again outside that cache, once per
widget per snapshot. It reads through the cache now. Render objects are
mutable, so they keep filling on every read.

A switched off timeline also stopped dumping the widget tree for events
it drops on the floor.
main moved in parallel on the same files: it swapped dart:html for
universal_web, split the report writer into an io and a web half to keep
dart:io out of spot.dart's import graph, and added browser tests that drive
the report.

- The report writer of this branch now lives in print_html_io.dart, behind
  main's conditional export. The web half gains a no-op deleteHtmlReport.
- _onPostTest keeps main's font-change pump around the report rendering, and
  records the test failure event inside it, because its capture can load the
  annotation font too.
- expandable_box.dart and modal.dart stay deleted. Their test goes with them.
- The two browser tests are pointed at the new UI: the lightbox instead of the
  modal, the capture pane instead of thumbnails. The report declares no <base>
  at all now (Document(base: null)), so that is what they assert.
- jaspr_test follows jaspr to 0.23.
unnecessary_underscores and use_null_aware_elements only fire from language
version 3.7 and 3.8 on, and CI analyzes with --fatal-infos.
Raising the SDK floor to 3.8 switches dart format to the tall style, and CI
runs it with --set-exit-if-changed over the whole package.
Flutter 3.47 reports prefer_if_elements_to_conditional_expressions for the
three places the track picks between a frame and a gap, and CI analyzes with
--fatal-infos. The compiled client is unchanged in behaviour and not
regenerated.
flutter test --platform chrome compiles every test file, also the ones that
only run on the VM, and it cannot resolve an import that leaves test/. The
test imported tool/serve_timeline.dart, which failed the whole Chrome run at
compile time.

serveTimeline now lives in lib/src/timeline/html, the tool keeps the argument
parsing, and the test is marked VM only because it binds a socket.
The printed link is 'file://' in front of a native path. On Windows that is
file://D:\a\..., which Uri.parse reads as host 'D', and the test then looked
for events.json on a network share.
On Flutter 3.32, the declared minimum, FilledButton.tonalIcon and
OutlinedButton.icon build a private subclass. spot<FilledButton>() matches the
exact type, so the demo could not find its own buttons there.
ide_link, source_excerpt and stale_report read files or start processes, and
failed every test on Chrome, compiled to JavaScript and to Wasm alike.

The VS Code link test also expected a POSIX path. The link holds the native
absolute path, which starts with a drive letter on Windows.
The formatter of Dart 3.12 lays these out differently, and CI formats with
the current stable.
A run that reports nothing deletes the previous report, and the report
directory is named after the test alone. A passing test therefore deleted the
failure report of a test with the same name in another file, moments after
its link was printed.

The report now records which test file wrote it, and only that file removes
it again. Two such tests still overwrite each other's report when both fail.
The failure event kept the first error FlutterError.onError saw. A test that
expects an error and takes it with takeException() carries on, and when it
failed later the report showed the expected error, with the tree and capture
of that earlier moment.

flutter_test cannot be asked whether an exception is still pending, so when a
second error arrives the pending one is taken and, if there was one, handed
straight back. Nothing pending means the test took the first error, and the
new one replaces it.
The browser tests rendered hand-built events that carry their widget tree in
plain. The writer compresses the tree and stores it once per frame, which
only the client app unpacks, and nothing covered that path end to end.
The entries sat under 0.20.0, which is released. Adds the breaking changes:
the Dart 3.8 / Flutter 3.32 floor and the new TimelineEvent parameters.
The gap columns were rewritten as if elements without regenerating the
bundle. Same behaviour, now built from the checked-in source again.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant