Skip to content

Conversation

hotzenklotz
Copy link
Member

@hotzenklotz hotzenklotz commented Apr 23, 2025

Updated screenshot tests to use vitest instead of ava. Removed tools/test.sh and all references to it. Running the screenshot tests on BrowserStack took 5min 25s which is still faster then our nightly run (ca. 9min).

Removed the following dev dependencies:

  • ava
  • node-fetch
  • c8 (frontend testing coverage; vitest uses the node.js builtin v8 coverage)
  • mock-require

vitest has an experimental feature called Browser Mode, which will run a test suite inside a browser either through playwright or webdriverio. Since we are using puppeteer I decided to not use browser mode and not change our current setup. Perhaps, this is feature is something we might want to explore in the future.

Steps to test:

  • Run WK_AUTH_TOKEN=... BROWSERSTACK_USERNAME=... BROWSERSTACK_ACCESS_KEY=... yarn test-screenshot
  • Run WK_AUTH_TOKEN=... BROWSERSTACK_USERNAME=... BROWSERSTACK_ACCESS_KEY=... yarn test-wkorg-screenshot

TODOs:

  • Find a good way to update screenshots and snapshots. Maybe a dedicated GitHub Actions workflow. (Follow up)

Issues:


(Please delete unneeded items, merge only when none are left open)

@hotzenklotz hotzenklotz self-assigned this Apr 23, 2025
Copy link
Contributor

coderabbitai bot commented Apr 23, 2025

📝 Walkthrough

"""

Walkthrough

This set of changes migrates the frontend JavaScript/TypeScript test suite from the AVA testing framework to Vitest, specifically targeting Puppeteer-based screenshot and E2E tests. The migration includes refactoring test files to use Vitest's lifecycle hooks, assertion styles, and context handling. Supporting utilities and helper functions are updated to remove AVA-specific constructs and adopt Vitest-compatible patterns. The package.json is updated to remove AVA and c8 dependencies, scripts, and configuration, replacing them with Vitest equivalents. The custom test preparation shell script is deleted, and the Vitest configuration is adjusted to include the relevant test files directly. No changes are made to application logic or exported entities outside of the test infrastructure.

Changes

Files/Paths Change Summary
frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.ts,
frontend/javascripts/test/puppeteer/dataset_rendering.wkorg_screenshot.ts,
frontend/javascripts/test/puppeteer/dtype_datasets_rendering.screenshot.ts
Migrated test suites from AVA to Vitest, updating lifecycle hooks, assertion styles, and context usage. Sequential and before/after hooks are rewritten to use Vitest constructs. Test logic, retry, and screenshot comparison are preserved. Imports updated to remove AVA and add Vitest.
frontend/javascripts/test/puppeteer/dataset_rendering_helpers.ts Refactored helper utilities to remove AVA-specific context and functions, replaced with Vitest-compatible types and hooks. Split setup/teardown into separate functions, removed unused exports, and updated global mocking for compatibility.
frontend/javascripts/test/backend-snapshot-tests/projects.e2e.ts Added a blank line after the beforeEach hook for readability; no logic or test behavior changes.
frontend/javascripts/test/e2e-setup.ts Clarified a comment regarding mocking behavior for the request library in unit tests versus E2E tests. No functional or logic changes.
package.json Removed ava and c8 from devDependencies, deleted related configuration blocks, and replaced test scripts with direct Vitest commands.
tools/test.sh Deleted the Bash script previously used for test preparation and running AVA-based tests.
vitest.config.ts Updated test file inclusion patterns to cover Puppeteer screenshot tests, removed exclusion of the Puppeteer directory, and reformatted coverage reporter configuration.

Assessment against linked issues

Objective Addressed Explanation
Replace AVA with Vitest for all test suites and infrastructure (#8454)
Remove reliance on mock-require and deprecated mocking patterns in test setup (#8454)
Eliminate the extra compilation step and shell script for test preparation and running (#8454)
Update test scripts and configuration in package.json to use Vitest and remove AVA/c8 configs (#8454)
Improve stack trace accuracy and support for ES modules in tests by removing AVA and custom compilation (#8454)

Possibly related PRs

  • Convert E2E tests to vitest #8543: The main PR and the retrieved PR both focus on migrating end-to-end (E2E) tests from the AVA framework to Vitest, involving the same test files and test setup code, indicating a direct and strong connection at the code level.
  • Always Upload Screenshot Artifacts for Nightly Tests #8171: Enhances BrowserStack session naming and logging in Puppeteer test helpers, overlapping with this PR's refactoring of the same helper files.
  • Fix screenshot tests #8289: Fixes URL handling and enforces sequential execution in AVA-based dataset rendering screenshot tests, related to the same test files migrated here.

Suggested labels

CI

Suggested reviewers

  • philippotto

Poem

🐇✨
Vitest is here, AVA hops away,
Screenshot tests now run the Vitest way.
No more shell scripts, no more fuss,
Just modern hooks—much less muss!
Helpers refactored, configs are neat,
With every test, the code feels fleet.
Hooray for change—let’s thump our feet!

"""


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 849bdfc and 9f31d74.

📒 Files selected for processing (1)
  • CHANGELOG.unreleased.md (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.unreleased.md
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: backend-tests
  • GitHub Check: frontend-tests
  • GitHub Check: build-smoketest-push

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@hotzenklotz hotzenklotz changed the base branch from master to vitest-e2e April 23, 2025 13:31
Base automatically changed from vitest-e2e to master April 24, 2025 08:23
}
`;

exports[`Project API > createProject and deleteProject 1`] = `
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test suite was renamed from Project API to Project API (E2E). Hence, the snapshots were marked and reported as obsolete by vitest.

t.context.browser = browser;
console.log(`\nBrowserStack Session Id ${await getBrowserstackSessionId(browser)}\n`);
}
export async function setupBeforeEach(context: ScreenshotTestContext, semaphore?: Semaphore) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is mostly identical just with different indentation. I split the method setupBeforeEachAndAfterEach() into two separate setupBeforeEach() and setupAfterEach() methods.

@hotzenklotz hotzenklotz changed the title WIP Update screenshot tests to use Vitest Update screenshot tests to use Vitest Apr 24, 2025
@hotzenklotz hotzenklotz marked this pull request as ready for review April 24, 2025 09:39
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
frontend/javascripts/test/puppeteer/dtype_datasets_rendering.screenshot.ts (1)

268-277: Minor: reuse changedPixels variable instead of shadowing

Inside the selective-segment branch you redeclare const changedPixels, shadowing the outer variable.
This is harmless but a little confusing; consider re-using the original name or renaming the inner variable.

-const changedPixels = await compareScreenshot(
+const selectiveChangedPixels = await compareScreenshot(
frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.ts (2)

188-200: Mismatch between file name used for screenshots and error message

The diff/new screenshots are saved under
annotation_${datasetName}_${fallbackLabel}.*.png
but the failure message refers to
annotation_${datasetName}_${fallbackLayerName}.*.png.

This confuses debugging because the suggested file names do not exist.

-`Annotation for dataset with name: "${datasetName}" does not look the same, see annotation_${datasetName}_${fallbackLayerName}.diff.png for the difference and annotation_${datasetName}_${fallbackLayerName}.new.png for the new screenshot.`
+`Annotation for dataset with name: "${datasetName}" does not look the same, see annotation_${datasetName}_${fallbackLabel}.diff.png for the difference and annotation_${datasetName}_${fallbackLabel}.new.png for the new screenshot.`

118-157: withRetry silently skips exceptions – consider catching errors to actually retry

withRetry only retries when the callback resolves to false. Any thrown exception aborts the whole test immediately, defeating the purpose of the retry wrapper (common when Puppeteer navigation fails or BrowserStack hiccups).

At each call site you could wrap the logic:

await withRetry(3, async () => {
  try {
    // existing screenshot logic…
    return isPixelEquivalent(changedPixels, width, height);
  } catch {
    return false;           // force retry
  }
}, (condition) => {  });

Alternatively, enhance withRetry in the helper to do the try/catch once centrally.
This will save flaky CI runs without masking genuine pixel mismatches.

Also applies to: 162-202, 206-243, 245-318, 320-357

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 636412e and cd9c1ae.

⛔ Files ignored due to path filters (2)
  • frontend/javascripts/test/backend-snapshot-tests/__snapshots__/projects.e2e.ts.snap is excluded by !**/*.snap
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (9)
  • frontend/javascripts/test/backend-snapshot-tests/projects.e2e.ts (1 hunks)
  • frontend/javascripts/test/e2e-setup.ts (1 hunks)
  • frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.ts (2 hunks)
  • frontend/javascripts/test/puppeteer/dataset_rendering.wkorg_screenshot.ts (2 hunks)
  • frontend/javascripts/test/puppeteer/dataset_rendering_helpers.ts (15 hunks)
  • frontend/javascripts/test/puppeteer/dtype_datasets_rendering.screenshot.ts (3 hunks)
  • package.json (2 hunks)
  • tools/test.sh (0 hunks)
  • vitest.config.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • tools/test.sh
🧰 Additional context used
🧬 Code Graph Analysis (3)
frontend/javascripts/test/puppeteer/dataset_rendering.wkorg_screenshot.ts (2)
frontend/javascripts/test/puppeteer/dataset_rendering_helpers.ts (7)
  • process (22-22)
  • ScreenshotTestContext (440-443)
  • setupBeforeEach (445-489)
  • setupAfterEach (491-496)
  • withRetry (421-437)
  • getNewPage (406-419)
  • screenshotDatasetView (153-163)
frontend/javascripts/test/puppeteer/screenshot_helpers.ts (2)
  • compareScreenshot (87-143)
  • isPixelEquivalent (30-35)
frontend/javascripts/test/puppeteer/dtype_datasets_rendering.screenshot.ts (4)
frontend/javascripts/test/puppeteer/dataset_rendering_helpers.ts (9)
  • ScreenshotTestContext (440-443)
  • setupBeforeEach (445-489)
  • setupAfterEach (491-496)
  • writeDatasetNameToIdMapping (51-71)
  • createAnnotationForDatasetScreenshot (73-76)
  • getNewPage (406-419)
  • withRetry (421-437)
  • screenshotDataset (85-99)
  • screenshotTracingView (352-404)
frontend/javascripts/oxalis/model/actions/settings_actions.ts (3)
  • updateDatasetSettingAction (65-73)
  • updateTemporarySettingAction (75-83)
  • updateLayerSettingAction (91-101)
frontend/javascripts/oxalis/model/actions/flycam_actions.ts (2)
  • setPositionAction (76-81)
  • setZoomStepAction (70-74)
frontend/javascripts/test/puppeteer/screenshot_helpers.ts (3)
  • compareScreenshot (87-143)
  • SCREENSHOTS_BASE_PATH (6-9)
  • isPixelEquivalent (30-35)
frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.ts (2)
frontend/javascripts/test/puppeteer/dataset_rendering_helpers.ts (11)
  • ScreenshotTestContext (440-443)
  • setupBeforeEach (445-489)
  • setupAfterEach (491-496)
  • writeDatasetNameToIdMapping (51-71)
  • withRetry (421-437)
  • getNewPage (406-419)
  • screenshotDataset (85-99)
  • screenshotAnnotation (101-126)
  • screenshotDatasetWithMapping (165-188)
  • screenshotDatasetWithMappingLink (189-209)
  • screenshotSandboxWithMappingLink (210-220)
frontend/javascripts/test/puppeteer/screenshot_helpers.ts (3)
  • compareScreenshot (87-143)
  • SCREENSHOTS_BASE_PATH (6-9)
  • isPixelEquivalent (30-35)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build-smoketest-push
  • GitHub Check: backend-tests
🔇 Additional comments (11)
frontend/javascripts/test/backend-snapshot-tests/projects.e2e.ts (1)

22-22: Style consistency addition

This blank line addition improves readability by maintaining consistent spacing between the beforeEach hook and the test cases, aligning with the Vitest migration's style conventions.

frontend/javascripts/test/e2e-setup.ts (1)

11-11: Improved mocking behavior documentation

The updated comment provides clearer explanation of the mocking behavior for the request library, correctly distinguishing between unit tests (globally mocked) and E2E tests (using original implementation). This clarification is valuable during the testing framework migration.

vitest.config.ts (2)

8-12: Test configuration properly updated for Puppeteer screenshot tests

The configuration now correctly includes Puppeteer screenshot test files by extending the include patterns and removing the previous exclusions. This change is essential for the AVA to Vitest migration of the screenshot tests.

#!/bin/bash
# Verify if all screenshot test files are covered by the new include patterns

# Find all wkorg_screenshot and screenshot test files in the puppeteer directory
echo "Screenshot test files that should be included:"
find ./frontend/javascripts/test/puppeteer -name "*.wkorg_screenshot.ts" -o -name "*.screenshot.ts"

echo -e "\nVerifying if patterns match all test files:"
find ./frontend/javascripts/test/puppeteer -name "*.wkorg_screenshot.ts" -o -name "*.screenshot.ts" | xargs -I{} basename {} | grep -E "\.((wkorg_screenshot)|(screenshot))\.ts$"

16-16: Format consistency in coverage reporter configuration

Switching from single to double quotes in the coverage reporter array maintains consistent formatting style with the rest of the configuration file.

frontend/javascripts/test/puppeteer/dataset_rendering.wkorg_screenshot.ts (4)

6-9: Updated imports to support Vitest test context

These import changes correctly adapt to the new helper function structure created for Vitest, importing the type ScreenshotTestContext and the separated setupBeforeEach and setupAfterEach functions.


12-12: Added Vitest test constructs

Appropriately replaced AVA imports with Vitest's testing constructs, supporting the migration from AVA to Vitest testing framework.


45-53: Test structure migrated to Vitest format

Successfully refactored the test structure to use Vitest's describe block with proper beforeEach and afterEach hooks. The implementation correctly uses the new helper functions setupBeforeEach and setupAfterEach with the appropriate test context typing.


54-91: Test case properly migrated to Vitest format

The test case has been successfully migrated from AVA's test.serial to Vitest's it.sequential with proper context handling. The assertion style has been updated from AVA's t.true to Vitest's expect().toBe(true) while maintaining the same testing logic and error messages.

package.json (1)

92-93: Screenshot tests may run concurrently across workers – consider disabling parallelism

vitest run will spawn several worker-threads by default.
Because every screenshot test launches a full Chrome/BrowserStack session, parallel workers can

  1. exceed the BrowserStack concurrency limit,
  2. overload the local machine (many Chrome instances),
  3. ignore the semaphore in your test file (it is only process-local and therefore not shared across workers).

Align the screenshot script with the existing test-e2e script by turning off parallelism:

-"test-screenshot": "vitest run --testTimeout=300000 frontend/javascripts/test/puppeteer/**/*.screenshot.ts",
-"test-wkorg-screenshot": "vitest run --testTimeout=300000 frontend/javascripts/test/puppeteer/**/*.wkorg_screenshot.ts",
+"test-screenshot": "vitest run --fileParallelism=false --threads=false --testTimeout=300000 frontend/javascripts/test/puppeteer/**/*.screenshot.ts",
+"test-wkorg-screenshot": "vitest run --fileParallelism=false --threads=false --testTimeout=300000 frontend/javascripts/test/puppeteer/**/*.wkorg_screenshot.ts",

Please verify that turning parallelism off is acceptable for CI runtime.

frontend/javascripts/test/puppeteer/dtype_datasets_rendering.screenshot.ts (1)

189-197: Semaphore does not prevent cross-worker concurrency

beforeEach acquires a Semaphore(1), but the semaphore instance lives only inside the current worker process.
If Vitest runs this file in multiple workers (default), other workers will create their own semaphore with the same capacity, so several browsers can start in parallel.

Unless you disable worker threads (see package.json comment) you can still get race conditions / BrowserStack quota errors.

No code change needed here if you adopt --threads=false, otherwise use a cross-process lock (e.g. proper-lockfile on the FS).

frontend/javascripts/test/puppeteer/dataset_rendering_helpers.ts (1)

445-489: Semaphore release may be skipped on test failure

setupAfterEach releases the semaphore, but it only runs after a test hook
completes successfully
. If the test crashes before finishing (e.g., unhandled
exception inside Puppeteer), afterEach is still executed, but if the process
crashes or Vitest is interrupted the lock file may persist across retries.

Consider wrapping the test body in try/finally or moving the release logic
into a finally block inside the test body to guarantee cleanup.

Comment on lines +15 to +20
import { vi, type TestContext } from "vitest";

vi.mock("libs/request", async (importOriginal) => {
// The request lib is globally mocked for the unit tests. In the screenshot tests, we actually want to run the proper fetch calls so we revert to the original implementation
return await importOriginal();
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Check correctness of vi.mock factory signature

Vitest passes no argument to the factory function – to access the real module you should call
vi.importActual() inside the factory.
Calling the (undefined) importOriginal parameter will throw at runtime.

-vi.mock("libs/request", async (importOriginal) => {
-  return await importOriginal();
+vi.mock("libs/request", async () => {
+  const actual = await vi.importActual<typeof import("libs/request")>("libs/request");
+  return actual;
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { vi, type TestContext } from "vitest";
vi.mock("libs/request", async (importOriginal) => {
// The request lib is globally mocked for the unit tests. In the screenshot tests, we actually want to run the proper fetch calls so we revert to the original implementation
return await importOriginal();
});
import { vi, type TestContext } from "vitest";
vi.mock("libs/request", async () => {
// The request lib is globally mocked for the unit tests. In the screenshot tests, we actually want to run the proper fetch calls so we revert to the original implementation
const actual = await vi.importActual<typeof import("libs/request")>("libs/request");
return actual;
});

Comment on lines 445 to 489
export async function setupBeforeEach(context: ScreenshotTestContext, semaphore?: Semaphore) {
if (semaphore) {
context.release = await semaphore.acquire();
}
if (USE_LOCAL_CHROME) {
// Use this for connecting to local Chrome browser instance
context.browser = await puppeteer.launch({
args: HEADLESS
? [
"--headless=false",
"--hide-scrollbars",
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--use-angle=gl-egl",
]
: [],
headless: HEADLESS ? "new" : false, // use "new" to suppress warnings
dumpio: true,
executablePath: "/usr/bin/google-chrome", // Linux; this might need to be adapted to your local setup
// executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", // Mac
});
} else {
checkBrowserstackCredentials();

const caps = {
browser: "chrome",
browser_version: "latest",
os: "os x",
os_version: "mojave",
name: context.task.name, // add test name to BrowserStack session
"browserstack.username": process.env.BROWSERSTACK_USERNAME,
"browserstack.accessKey": process.env.BROWSERSTACK_ACCESS_KEY,
};
const browser = await puppeteer.connect({
browserWSEndpoint: `ws://cdp.browserstack.com/puppeteer?caps=${encodeURIComponent(
JSON.stringify(caps),
)}`,
});
context.browser = browser;
console.log(`\nBrowserStack Session Id ${await getBrowserstackSessionId(browser)}\n`);
}

console.log(`\nRunning chrome version ${await t.context.browser.version()}\n`);
global.Headers = Headers;
global.fetch = fetch;
global.Request = Request;
global.Response = Response;
// @ts-expect-error ts-migrate(7017) FIXME: Element implicitly has an 'any' type because type ... Remove this comment to see the full error message
global.FetchError = FetchError;
});
console.log(`\nRunning chrome version ${await context.browser.version()}\n`);
}
Copy link
Contributor

@coderabbitai coderabbitai bot Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

HEADLESS flags contradict each other

When HEADLESS === true you pass the argument --headless=false and
headless: "new". The CLI flag overrides the option object, resulting in a
non-headless browser although the constant says otherwise.

Either invert the flag or drop the argument list:

-args: HEADLESS
-  ? [
-      "--headless=false",
+args: HEADLESS
+  ? [
+      "--headless=new",

(or build the args array only when !HEADLESS).

This avoids confusion and makes local debugging easier.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function setupBeforeEach(context: ScreenshotTestContext, semaphore?: Semaphore) {
if (semaphore) {
context.release = await semaphore.acquire();
}
if (USE_LOCAL_CHROME) {
// Use this for connecting to local Chrome browser instance
context.browser = await puppeteer.launch({
args: HEADLESS
? [
"--headless=false",
"--hide-scrollbars",
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--use-angle=gl-egl",
]
: [],
headless: HEADLESS ? "new" : false, // use "new" to suppress warnings
dumpio: true,
executablePath: "/usr/bin/google-chrome", // Linux; this might need to be adapted to your local setup
// executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", // Mac
});
} else {
checkBrowserstackCredentials();
const caps = {
browser: "chrome",
browser_version: "latest",
os: "os x",
os_version: "mojave",
name: context.task.name, // add test name to BrowserStack session
"browserstack.username": process.env.BROWSERSTACK_USERNAME,
"browserstack.accessKey": process.env.BROWSERSTACK_ACCESS_KEY,
};
const browser = await puppeteer.connect({
browserWSEndpoint: `ws://cdp.browserstack.com/puppeteer?caps=${encodeURIComponent(
JSON.stringify(caps),
)}`,
});
context.browser = browser;
console.log(`\nBrowserStack Session Id ${await getBrowserstackSessionId(browser)}\n`);
}
console.log(`\nRunning chrome version ${await t.context.browser.version()}\n`);
global.Headers = Headers;
global.fetch = fetch;
global.Request = Request;
global.Response = Response;
// @ts-expect-error ts-migrate(7017) FIXME: Element implicitly has an 'any' type because type ... Remove this comment to see the full error message
global.FetchError = FetchError;
});
console.log(`\nRunning chrome version ${await context.browser.version()}\n`);
}
export async function setupBeforeEach(context: ScreenshotTestContext, semaphore?: Semaphore) {
if (semaphore) {
context.release = await semaphore.acquire();
}
if (USE_LOCAL_CHROME) {
// Use this for connecting to local Chrome browser instance
context.browser = await puppeteer.launch({
args: HEADLESS
? [
"--headless=new",
"--hide-scrollbars",
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--use-angle=gl-egl",
]
: [],
headless: HEADLESS ? "new" : false, // use "new" to suppress warnings
dumpio: true,
executablePath: "/usr/bin/google-chrome", // Linux; this might need to be adapted to your local setup
// executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", // Mac
});
} else {
checkBrowserstackCredentials();
const caps = {
browser: "chrome",
browser_version: "latest",
os: "os x",
os_version: "mojave",
name: context.task.name, // add test name to BrowserStack session
"browserstack.username": process.env.BROWSERSTACK_USERNAME,
"browserstack.accessKey": process.env.BROWSERSTACK_ACCESS_KEY,
};
const browser = await puppeteer.connect({
browserWSEndpoint: `ws://cdp.browserstack.com/puppeteer?caps=${encodeURIComponent(
JSON.stringify(caps),
)}`,
});
context.browser = browser;
console.log(`\nBrowserStack Session Id ${await getBrowserstackSessionId(browser)}\n`);
}
console.log(`\nRunning chrome version ${await context.browser.version()}\n`);
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@philippotto I did not change the headless browser setup but the CodeRabbit comment is valid. Perhaps we remove line 454 ("--headless=false")?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember that there was some api change in puppeteer that required this weird headless: "new" stuff. back then, I briefly fiddled with it and the current way worked for both headless and not headless (useful for seeing the actual browser window). I cannot judge whether coderabbits suggestion works, but feel free to try it out. just set the local chrome flag to true and test both headless and "headfull".

Comment on lines 97 to 105
describe("Dataset Rendering", () => {
beforeEach<ScreenshotTestContext>(async (context) => {
await setupBeforeEach(context);
});

afterEach<ScreenshotTestContext>(async (context) => {
await setupAfterEach(context);
});

Copy link
Contributor

@coderabbitai coderabbitai bot Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consider limiting browser concurrency to avoid resource exhaustion

beforeEach launches a fresh Chromium instance for every single test case. Even though each individual test is flagged as it.sequential, Vitest may still run multiple files in parallel, which can easily exhaust local CI resources or BrowserStack quotas.

If you intend to guarantee a single browser at any given time, pass in the semaphore from dataset_rendering_helpers (or create a simple one here) and acquire it in setupBeforeEach.

Example patch (sketch):

-beforeEach<ScreenshotTestContext>(async (context) => {
-  await setupBeforeEach(context);
-});
+const browserSemaphore = new Semaphore(1); // 1 == fully serialise across files
+
+beforeEach<ScreenshotTestContext>(async (context) => {
+  await setupBeforeEach(context, browserSemaphore);
+});

This keeps the behaviour identical on a developer machine while preventing accidental parallel launches in CI.


🏁 Script executed:

#!/bin/bash
# Locate the helpers file and inspect setupBeforeEach definition and signature
rg -n "dataset_rendering_helpers"  
rg -n "setupBeforeEach" -n  

Length of output: 1191


🏁 Script executed:

#!/bin/bash
# Inspect Semaphore import and setupBeforeEach/AfterEach definitions
rg -n -A5 -B5 "import.*Semaphore" frontend/javascripts/test/puppeteer/dataset_rendering_helpers.ts
rg -n -A5 -B5 "export async function setupBeforeEach" frontend/javascripts/test/puppeteer/dataset_rendering_helpers.ts
rg -n -A5 -B5 "export async function setupAfterEach" frontend/javascripts/test/puppeteer/dataset_rendering_helpers.ts

Length of output: 1749


Limit browser concurrency to prevent CI resource exhaustion

To serialize Chromium launches across this test file (and avoid exhausting CI or BrowserStack quotas), import and instantiate a Semaphore and pass it into your existing setupBeforeEach call—mirroring the pattern used in dtype_datasets_rendering.screenshot.ts.

• File: frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.ts
– Add an import for the runtime Semaphore class
– Instantiate a single-permit semaphore at module scope
– Pass that semaphore into setupBeforeEach

--- a/frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.ts
+++ b/frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.ts
@@ 1,6 +1,9 @@
 import { describe, it, beforeEach, afterEach } from "vitest";
 import { setupBeforeEach, setupAfterEach } from "./dataset_rendering_helpers";
+import Semaphore from "semaphore-promise";
+
+const browserSemaphore = new Semaphore(1);

 describe("Dataset Rendering", () => {
-  beforeEach<ScreenshotTestContext>(async (context) => {
-    await setupBeforeEach(context);
-  });
+  beforeEach<ScreenshotTestContext>(async (context) => {
+    await setupBeforeEach(context, browserSemaphore);
+  });

   afterEach<ScreenshotTestContext>(async (context) => {
     await setupAfterEach(context);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe("Dataset Rendering", () => {
beforeEach<ScreenshotTestContext>(async (context) => {
await setupBeforeEach(context);
});
afterEach<ScreenshotTestContext>(async (context) => {
await setupAfterEach(context);
});
// frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.ts
import { describe, it, beforeEach, afterEach } from "vitest";
import { setupBeforeEach, setupAfterEach } from "./dataset_rendering_helpers";
+import Semaphore from "semaphore-promise";
+
+const browserSemaphore = new Semaphore(1);
describe("Dataset Rendering", () => {
- beforeEach<ScreenshotTestContext>(async (context) => {
- await setupBeforeEach(context);
- });
+ beforeEach<ScreenshotTestContext>(async (context) => {
+ await setupBeforeEach(context, browserSemaphore);
+ });
afterEach<ScreenshotTestContext>(async (context) => {
await setupAfterEach(context);
});
// …rest of your tests…
});

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@philippotto What do you think about this? This sounds reasonable to me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially added the semaphore logic to the dtype test, because I tested it locally and in parallel, but didn't have enough ram for running all tests in parallel (in the end, I apparently committed 1 for the semaphore value which is identical to running them serially).
in theory, running all these specs in parallel should work fine with browserstack (so, no sequential and no semaphore). this would probably make the tests a bit faster.
to summarize I see these two options:
A) make/keep everything sequential and remove the semaphore
B) make/keep everything concurrent but use the semaphore to limit it to sane limits (e.g., 2 if localhost else 5?)

not sure whether B will also work for the normal dataset_rendering.screenshot.ts (because it also changes the view config, I think), but it should definitely work for the dtypes specs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Then the default vitest behavior (sequential execution of all tests within on suite) should almost cover case A) . I can remove the semaphore to make the code easier.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the semaphore and switched to vitest's test.for API instead of loops to ensure proper test setup/teardown. https://vitest.dev/api/#test-for

hotzenklotz and others added 2 commits April 24, 2025 11:48
…ot.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Member

@philippotto philippotto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome, looks good to me :)

@hotzenklotz hotzenklotz enabled auto-merge (squash) April 25, 2025 08:29
@hotzenklotz hotzenklotz merged commit 2eea8a6 into master Apr 25, 2025
5 checks passed
@hotzenklotz hotzenklotz deleted the vitest-screenshot branch April 25, 2025 08:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Technical debt in JS test set up
2 participants