test(e2e): scroll the grid so the NOTES column is on screen for the siphonophore beat
The three-row siphonophore result is narrower than the grid viewport, so Handsontable sizes the columns wider than it (scrollWidth 1562 vs clientWidth 1340): a horizontal scrollbar appears and NOTES - the column the match is actually in - is clipped. The cell text is in the DOM either way, so the beat passed without it, but the demo read as three arbitrary rows. scrollGridTo() sets scrollLeft on #hotTable .wtHolder, asserts the value landed, and the beat asserts the full NOTES text and scrolls back for the following beats. It is a plain spec step - CI runs it, no recording flag and no cy.wait. Also lets a recording size the browser window: the capture is the window's content area, so RECORD_WINDOW_SIZE is passed through as --window-size for a real Chromium browser (Electron ignores it). That is what makes a 16:9 pane possible: (W-450)/(H-96) = 16/9 at 1920x923 of content, i.e. 1920x1010 of window, for a 1470x827 pane and the repo's 1600x900 viewport at ~0.90 zoom. Inert unless the env var is set, so CI is unaffected. dc-cypress updated with the window-sizing recipe, the clipped-column scroll and the frame mean/variance scan used to find the cut point.
This commit is contained in:
1 parent
37c6e50cb6
commit
aae48dc401
3 files changed
+87
-13
No files matched your search
@@ -81,27 +81,47 @@ Don't use for: SAS backend tests (`sasjs test -t 4gl`), Angular unit tests (`npm
|
||||
|
||||
## Recording a demo video
|
||||
|
||||
`video: true` in `cypress.config.ts` is all that is needed. Run the spec with `--browser electron` and the MP4 lands in `client/cypress/videos/<spec>.mp4`.
|
||||
`video: true` in `cypress.config.ts` is all that is needed. Run the spec and the MP4 lands in `client/cypress/videos/<spec>.mp4`.
|
||||
|
||||
**The capture is a fixed 1280x720 window, and it contains the Cypress runner** - the command log takes the left ~450px and the app is rendered in the remaining pane. Nothing changes that: `viewportWidth`/`viewportHeight`, `--force-device-scale-factor`, a CSS `zoom`, a Chrome-for-Testing binary, and a 2560x1440 Xvfb display were all tested and all leave the frame at 1280x720 (Electron) or 1280x632 (Chrome). Cypress starts its own Xvfb for the run (a second display appears in `/tmp/.X11-unix`), so an external `DISPLAY` is ignored.
|
||||
**The capture is the browser window's content area, and it contains the Cypress runner** - the command log takes the left ~450px and the app is rendered in the remaining pane, so the pane is `(capture width - 450) x (capture height - 96)` and the app is zoomed to fit it.
|
||||
|
||||
What the viewport config *does* control is the zoom the app is rendered at inside that pane - Cypress zooms the AUT to fit the pane. So a 1600x900 viewport renders at 50% and leaves most of the pane's height unused (the app occupies ~806x452 of it), while a viewport whose aspect ratio matches the pane (~830x624, i.e. ~1.33) fills it at ~75%:
|
||||
That makes the window size the only lever that matters:
|
||||
|
||||
npx cypress run --browser electron \
|
||||
--config viewportWidth=1100,viewportHeight=827 \
|
||||
--spec cypress/e2e/full-table-search.cy.ts --env skipNegative=true,demoPause=1500
|
||||
- Electron is stuck at 1280x720 (it ignores `--window-size`), i.e. an 830x624 pane, and the repo's 1600x900 viewport renders at 50% - the app only occupies ~806x452 of it.
|
||||
- A real Chromium browser honours `--window-size`, so the capture can be sized to whatever the pane needs. `cypress.config.ts` passes it through from `RECORD_WINDOW_SIZE` (inert in CI, which sets no env).
|
||||
|
||||
The app then occupies ~793x596 of the 1280x720 frame. Crop that region out and the runner disappears:
|
||||
For a 16:9 pane, solve `(W - 450) / (H - 96) = 16/9`: W = 1920 gives H = 923 of window *content*, and Chrome adds ~87px of window chrome above that, so pass 1920x1010:
|
||||
|
||||
ffmpeg -i cypress/videos/<spec>.mp4 -vf "crop=792:596:468:80" \
|
||||
RECORD_WINDOW_SIZE=1920,1010 npx cypress run \
|
||||
--browser /opt/data/profiles/dc/tools/chrome-linux64/chrome \
|
||||
--spec cypress/e2e/full-table-search.cy.ts \
|
||||
--env skipNegative=true,demoPause=1500
|
||||
|
||||
That yields a 1920x922 capture with a 1470x826 pane (16:9) and, at the repo's 1600x900 viewport, an app at ~0.90 zoom occupying 1441x812 - about 1.9x the linear size of the 16:9 crop Electron can give. Crop the app out:
|
||||
|
||||
ffmpeg -ss <start> -i cypress/videos/<spec>.mp4 -vf "crop=1440:812:464:80" \
|
||||
-an -c:v libx264 -preset slow -crf 18 -pix_fmt yuv420p \
|
||||
-movflags +faststart <spec>-apponly.mp4
|
||||
-movflags +faststart <spec>-cut.mp4
|
||||
|
||||
Measure the crop rect from the frames rather than guessing: the pane's flat background is rgb(225,227,236), so a row/column scan for pixels that differ from it gives the app's bounding box (runner pane ends x=449, app x=468-1259, app y=80-675 for a 1100x827 viewport).
|
||||
Measure the crop rect from the frames rather than guessing: the pane's flat background is rgb(225,227,236), so a row/column scan for pixels that differ from it gives the app's bounding box (for the 1920x922 capture: runner pane ends x=449, app x=463-1903, app y=80-893; for the 1280x720 Electron capture: app x=468-1259, y=80-675). The app's own background is nearly the same grey, so verify the result on the first, middle and last frames rather than trusting a pixel test alone.
|
||||
|
||||
To find where to cut, scan frame variance in the content area: the "Loading Table Viewer" spinner is a nearly flat frame (std dev ~13) and the rendered table is not (~29), so the first high-variance frame is where the table appears. Do not cut on a guessed timestamp - cutting 0.2s early shows a spinner flash.
|
||||
To find where to cut, scan the mean *and* standard deviation of the content area: the app boots mid-run (a dark screen, mean ~65, sd ~2.5), the "Loading Table Viewer" spinner is flat and light (mean ~252, sd ~0), and the rendered table is mean ~245, sd ~31. Cut on the first table frame - cutting a fraction early shows the boot screen or the spinner.
|
||||
|
||||
Chrome for Testing (the only non-Electron browser installable here - no system Chrome, no apt) works with Cypress via `--browser /path/to/chrome-linux64/chrome`, but its capture is 1280x632, i.e. a shorter AUT pane than Electron's. Use Electron.
|
||||
Cypress starts its own Xvfb for the run (a second display appears in `/tmp/.X11-unix`), so an external `DISPLAY` is ignored; `--force-device-scale-factor` and a CSS `zoom` also do not change the capture.
|
||||
|
||||
### Grid columns that are clipped
|
||||
|
||||
Handsontable sizes columns to content, so a narrow result can be *wider* than the grid viewport: the horizontal scrollbar appears and the rightmost column (NOTES on `MPE_X_SEARCH`) is cut off. The cell text is still in the DOM, so an assertion passes either way - the column only has to be scrolled into view for the *video* to show why the rows matched.
|
||||
|
||||
const scrollGridTo = (position: 'left' | 'right') => {
|
||||
cy.get('#hotTable .wtHolder').first().then(($holder: any) => {
|
||||
const el = $holder[0]
|
||||
el.scrollLeft = position === 'right' ? el.scrollWidth : 0
|
||||
expect(el.scrollLeft).to.equal(position === 'right' ? el.scrollWidth - el.clientWidth : 0)
|
||||
})
|
||||
}
|
||||
|
||||
Setting `scrollLeft` is enough - Handsontable keeps the header clone in step - and the browser clamps it to `scrollWidth - clientWidth`, so that is what scrolling right has to land on. This is a legitimate spec step (it runs in CI), not a recording flag.
|
||||
|
||||
### A longer, more readable demo
|
||||
|
||||
|
||||
@@ -38,9 +38,24 @@ export default defineConfig({
|
||||
// Pin the browser locale so locale-formatted cells (intl-date/time/datetime)
|
||||
// render deterministically regardless of the runner's system locale.
|
||||
on('before:browser:launch', (browser, launchOptions) => {
|
||||
// Recording only: the video capture is the browser window, which
|
||||
// contains the Cypress runner - the command log takes the left ~450px
|
||||
// and the app sits in the remaining pane. Sizing the window is the
|
||||
// only lever that moves the capture, and only for a real Chromium
|
||||
// browser (Electron ignores it). For a 16:9 pane, solve
|
||||
// (W - 450) / (H - 96) = 16/9: W = 1920 -> H = 923 of window content,
|
||||
// so pass RECORD_WINDOW_SIZE=1920,1010 and crop the pane.
|
||||
// Inert unless the env var is set, so CI is unaffected.
|
||||
if (process.env.RECORD_WINDOW_SIZE && browser.family === 'chromium') {
|
||||
launchOptions.args.push(
|
||||
`--window-size=${process.env.RECORD_WINDOW_SIZE}`
|
||||
)
|
||||
}
|
||||
|
||||
if (browser.family === 'chromium' && browser.name !== 'electron') {
|
||||
launchOptions.args.push('--lang=en-GB')
|
||||
}
|
||||
|
||||
return launchOptions
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,10 +83,17 @@ context('full table search tests: ', function () {
|
||||
|
||||
// Partial character search: "siphonophore" only ever appears in the middle
|
||||
// of the NOTES text, so a match proves the search is not column-specific.
|
||||
// Three rows is narrow enough that the grid grows a horizontal scrollbar
|
||||
// and clips NOTES - the column the match is actually in - so scroll it
|
||||
// into view, assert on what is now on screen, then put the grid back.
|
||||
searchFor('siphonophore')
|
||||
assertRowCount('3')
|
||||
scrollGridTo('right')
|
||||
pauseForDemo()
|
||||
assertGridContains(['Oceanus Rise', 'Nereid Trench', 'Halcyon Trench'])
|
||||
assertGridExcludes(['Kraken Hollow', 'Vostok Deep'])
|
||||
assertGridContains(['bioluminescent siphonophore was filmed here'])
|
||||
scrollGridTo('left')
|
||||
|
||||
// A search spans every column at once: "Halcyon" is both a site (Halcyon
|
||||
// Trench) and a vessel (RV Halcyon), and the 280 matches are the union.
|
||||
@@ -192,6 +199,12 @@ const visitPage = (url: string) => {
|
||||
// --env skipNegative=true,demoPause=1500
|
||||
const demoPauseMs = Number(Cypress.env('demoPause')) || 0
|
||||
|
||||
// Linger for a recording. A no-op unless demoPause is set, which only the
|
||||
// recording run does - CI passes no env.
|
||||
const pauseForDemo = () => {
|
||||
if (demoPauseMs > 0) cy.wait(demoPauseMs)
|
||||
}
|
||||
|
||||
// The row/column count the viewer shows next to the table name, e.g.
|
||||
// "(1,000 rows, 9 cols)". The viewer pluralises: a single-row result reads
|
||||
// "(1 row, 9 cols)".
|
||||
@@ -203,7 +216,33 @@ const assertRowCount = (rows: string) => {
|
||||
}
|
||||
)
|
||||
|
||||
if (demoPauseMs > 0) cy.wait(demoPauseMs)
|
||||
pauseForDemo()
|
||||
}
|
||||
|
||||
// The grid body is a Handsontable .wtHolder, and a narrow result is wider than
|
||||
// the grid viewport, which clips the rightmost column (NOTES). Setting
|
||||
// scrollLeft is enough - Handsontable keeps the header clone in step - and the
|
||||
// assertion right after proves the scroll landed rather than assuming it. The
|
||||
// browser clamps scrollLeft to (scrollWidth - clientWidth), so that is what
|
||||
// scrolling right has to land on.
|
||||
const scrollGridTo = (position: 'left' | 'right') => {
|
||||
cy.get('#hotTable .wtHolder')
|
||||
.first()
|
||||
.then(($holder: any) => {
|
||||
const el = $holder[0]
|
||||
const maxScroll = el.scrollWidth - el.clientWidth
|
||||
|
||||
el.scrollLeft = position === 'right' ? el.scrollWidth : 0
|
||||
|
||||
if (position === 'right') {
|
||||
// This step only means anything if the grid really is clipping a
|
||||
// column - fail loudly rather than silently doing nothing.
|
||||
expect(el.scrollWidth).to.be.greaterThan(el.clientWidth)
|
||||
expect(el.scrollLeft).to.equal(maxScroll)
|
||||
} else {
|
||||
expect(el.scrollLeft).to.equal(0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// A search that matches nothing replaces the grid with the viewer's "no data"
|
||||
|
||||
Reference in new issue
Block a user