Skip to content

examples: add a tennis scoreboard exercising dynamic steps and per-step action durations - #87

Open
bensynapse wants to merge 3 commits into
ebu:mainfrom
bensynapse:tennis-scoreboard-example
Open

bensynapse wants to merge 3 commits into
ebu:mainfrom
bensynapse:tennis-scoreboard-example

Conversation

@bensynapse

Copy link
Copy Markdown

I work on the Live Tennis API, a tennis data feed, which is how I came to read the specification closely. This is a specification example, not a product integration: the graphic contains no fetch, no XMLHttpRequest, no WebSocket, no imports and no API key, and it renders complete from its manifest schema defaults. Our feed is mentioned once, in the example's README, as one of three ways an operator might drive the fields, alongside a manual rundown or another provider. AI tooling was used while writing it; the testing below is mine.

v1/examples/scoreboard is football-shaped, with a fixed five-step sequence from pre-match to full-time, a minute capped at 90, and goal custom actions. So several parts of v1 currently have no worked example. Tennis reaches them naturally.

  • A dynamic step count. A tennis match's step sequence is one step per set plus a result, and the set count comes from the match-format data field: four steps for best of three, six for best of five. A fixed-count sport cannot motivate the dynamic value.
  • Per-step action durations. The board establishes itself onto set one in 620 milliseconds and transitions between later sets and the result in 400. The duration depends only on the target step, so the declared values hold whether the graphic is animating in or stepping while already visible.
  • A boolean property and an integer select with labels, for the serve indicator and the match format. The football example uses only strings, integers and colours.
  • Custom actions in both forms. Two are parameterless for one-click bindings, and two carry a typed payload schema so a controller can generate a form, returning 400 without a player. The existing example's actions are both parameterless.
  • State whose rendering depends on other state. The point counts are rally counts shown as 0, 15, 30, 40 or advantage with real deuce, or as a running count while the tiebreak flag is set. That is why they are not a five-value enumeration.
  • Hidden properties on the thirteen technical fields, so a controller labels the graphic with the event and the two players rather than a string of digits.

Points cascade as they do on court: a point can win the game, a game can win the set at six with two clear, seven-five or seven-six, six-all starts a tiebreak whose serve changes after the first point and then every two, and the break-point badge is derived from the score rather than supplied, so it cannot contradict it. The rules are four exported pure functions.

Registration, and a footprint question for you

The example is registered in the demo catalogue using your own tooling rather than by hand-editing the generated file list. Your tests couple catalogue membership to the landing page, since every catalogue example must have a carousel slide, so this also adds a slide and a tab driven by your existing generic controller, and a drawn court backdrop at 21 KB with no third-party asset.

That makes the diff larger than a pure example would be. If you would rather it stayed out of the landing page, say so and I will cut it back to the example directory and the catalogue entry alone.

Two expectations in your suite enumerated six catalogue examples and assumed Weather was the last card. I retargeted them rather than weakening them: "End" still means the last tab, and the focus-rescue assertion still checks that focus lands on the tab when Next becomes disabled on the final slide.

One design note. Your generic controller always plays step 0. My first step model made step 0 a pre-match card, which showed a scoreboard with no score in the demo, so I restructured it to make step 0 the live set. That is better for any renderer that simply plays the graphic, and it keeps the dynamic-step justification intact.

Proof that it makes no network call

Three independent lines. Statically, the module has no network, storage or import statements at all. At runtime, a new test imports it with every route aborted and the browser context offline, drives play and a custom action, asserts the rendered shadow DOM matches the schema defaults and a laid-out board, and then asserts the abort handler recorded nothing. And driven through your own demo player, the complete request list is the player, its scripts, the catalogue, the manifest, the module and the site's own background image, with no console errors.

Verification

npm --prefix website/_tooling run validate        Website validation passed
npm --prefix website/_tooling run test:browsers   36 passed (1.7m)

That is 36 of 36 across Chromium 151, Firefox 153 and WebKit 26.5, up from 32 of 32 before this change. The suite gains a cross-browser lifecycle test, which I tagged so it runs on all three engines rather than Chromium only, and the network-blocked render test. The thumbnail is 19 KB against the 500 KB limit, and the vendored-assets check stays clean.

What I could not verify

Your CI pins Node 24 and this machine has 22, so everything above ran on 22 and npm warned about the engine requirement. No Jekyll gem is installed either, so the browser tests ran against the repository root rather than the built site, meaning the one assertion that branches on that variable was not exercised in its CI form; it touches only pre-existing specification routes. And nothing has been verified against a real broadcast renderer, only against the specification, the schemas and your harness.

One observation for you, independent of this change: the accessibility test now scans one more carousel state and measured 39.4 seconds against its 45 second timeout on my machine, where a pristine main measured 41.1. The margin was already thin, so you may want to raise that timeout on its own merits.

The existing scoreboard example is football-shaped: five fixed steps from
pre-match to full-time, a minute capped at 90, and goal custom actions.
Tennis needs parts of the specification that shape cannot reach, so this
example covers them:

- stepCount -1, because the step sequence is one per set plus the result
  and the set count comes from the bestOf data field;
- actionDurations.steps, so the establishing transition onto set one can
  declare a different duration from the later set and result transitions;
- a boolean serve indicator, and a tie-break flag that switches the points
  from 0/15/30/40/AD to a running count;
- custom actions in both forms: two parameterless point actions and two
  that carry a GDD payload schema;
- hidden: true on the technical properties, so a controller labels the
  Graphic with the event and the two players only.

Points cascade as they do on court: a point can win the game, the game can
win the set, 6-6 starts a tie-break whose serve rotates every two points,
and the break-point badge is derived from the score rather than supplied,
so it cannot contradict it.

The Graphic reads only the data it is given. It has no fetch, no
XMLHttpRequest, no WebSocket, no imports and no external font or image, so
it renders complete from the manifest's schema defaults in an offline
renderer.

Registers the example in the demo catalogue with the generated file list, a
carousel card driven by the existing generic controller, and a drawn court
backdrop for the demo. The suite gains a cross-browser lifecycle test and a
test that renders the Graphic with the network blocked; the expectations
that enumerated six catalogue examples now cover seven, and the two demo
navigation assertions that relied on Weather being the last card now point
at the last card.
@Richardpwe

Copy link
Copy Markdown
Collaborator

This looks like a great addition to the example collection, especially with the dynamic steps and parameterised custom actions.
While testing the graphic locally, both by calling its OGraf methods directly and through the website demo, I noticed three edge cases:

  • When loading a tie-break already at 1–0 (gamesA: 6, gamesB: 6, pointsA: 1, pointsB: 0, tiebreak: true, playerAServing: false), then awarding A the next six points, the graphic shows A serving in the next set. I’d expect B here. Could this be related to initialising _tiebreakStartServerIsA from the current server?
  • Starting from the defaults at step 0, updateAction({ data: { pointsB: 3 }, skipAnimation: true }) still animates the break-point badge. It looks like the CSS pressureIn animation runs independently of skipAnimation.
  • In the website demo, clicking “Point” advances A’s score from 15 to 30, but the “Points A” field stays at 15. Changing only the player’s name and clicking “Update” then takes the score back to 15. This seems to come from the existing generic controller not reflecting the updated graphic state in the form fields.

One small thought on the website demo: since the card highlights “Dynamic steps”, it might be nice to let visitors explore the different set and result views, perhaps also with both match formats. Happy to help with the website integration if useful, just let me know.
Apart from these points, the example looks great!

@bensynapse

Copy link
Copy Markdown
Author

Thanks for the concrete cases. I reproduced all three and pushed fixes in 8f086bf:

  • Loading or correcting a tie-break now reconstructs its first server from the current server and the total points played. Your 1–0 example now starts the next set with B serving. Regressions cover both starting servers and snapshots at 0 through 6 points.
  • The pressure badge now obeys skipAnimation, including cancellation of an animation already running. Tests check both updates and custom actions.
  • The controller reflects returned score state in its fields while preserving unsent edits. Changing a name after awarding a point retains the score. The point fields now accept rally counts beyond four, so repeated deuce and tie-break scores can round-trip too.

I also added Previous/Next view controls and a best-of-three/five selector. Shortening the format while on a later view clamps it to the result; browser coverage exercises both formats.

Verification used Node 24 and the pinned Chromium, Firefox and WebKit versions. Website validation passed. The full browser run passed 48 of 50 cases; the two accessibility scans exceeded their existing 45-second timeout. Both passed on a serial rerun (test:browsers -- --last-failed --workers=1, 40.1s and 40.3s), with that timeout unchanged. All new regressions passed across the configured browsers.

These checks served the source tree; I have not exercised the Jekyll-built site or a broadcast renderer.

@nytamin

nytamin commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Looks like a very good example to add, thanks for taking the time to add it!

I also tested it just now, and I found a minor (potential) bug: When I send an update to the graphic it resets the score. (Though perhaps that was intentional?)

@bensynapse

Copy link
Copy Markdown
Author

The reset is not intended. I reproduced a remaining race in the website demo: click Point and then Update before the point animation finishes, and Update resends the old score from the form (30 becomes 15).

The controller now sends only edited fields when the graphic has returned state. This preserves points awarded during an animation, while explicit score edits, including zero, still work. The new browser regression reproduces the reset before the fix and checks both the name update and an intentional score correction afterward.

Pushed in 4bc7d42. Website validation and all 54 browser tests passed in the test container, including Chromium desktop/mobile, Firefox and WebKit (npm --prefix website/_tooling run validate and npm --prefix website/_tooling run test:browsers -- --workers=1). These checks served the source tree, not a Jekyll build or broadcast renderer.

@Richardpwe

Copy link
Copy Markdown
Collaborator

I re-tested the latest changes locally and can no longer reproduce the issues. The new demo controls work well too, and CI now covers the Jekyll-built site. Looks good to me!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3 participants