Vitest browser mode
Alpha
This entry point ships only in the alpha releases: npm i -D specflow-emulator@alpha.
specflow-emulator ships a dedicated entry point, specflow-emulator/browser, for
Vitest browser mode. Tests then run inside a
real browser, where fs / glob are not available, so two things change compared
to the Node setup:
- step definition files are discovered with Vite's
import.meta.globinstead ofglob; .featurefiles are passed todefineFeatureas text instead of a path.
Nothing else changes: defineSteps, the scenario context, scopes, tags and shared
steps all behave exactly like in the Node setup.
Installβ
npm i -D specflow-emulator@alpha @vitest/browser @vitest/browser-playwright playwright
npx playwright install chromium
Swap @vitest/browser-playwright / playwright for @vitest/browser-webdriverio /
webdriverio if you prefer the WebdriverIO provider (see
below).
Configurationβ
// vitest.config.js
import { defineConfig } from "vitest/config";
import { playwright } from "@vitest/browser-playwright";
import { specflowFeatures } from "specflow-emulator/vite";
export default defineConfig({
// Lets you `import feature from "./x.feature"` (see "Feature files" below).
plugins: [specflowFeatures()],
// Pre-bundle the parser deps so Vitest does not reload the page mid-run.
optimizeDeps: {
include: [
"@cucumber/gherkin",
"jest-cucumber/dist/src/configuration",
"jest-cucumber/dist/src/feature-definition-creation",
],
},
test: {
globals: true,
include: [
"**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}",
"**/*.steps.js",
],
setupFiles: ["./setupTests.js"],
browser: {
enabled: true,
headless: true,
provider: playwright(),
instances: [{ browser: "chromium" }],
},
},
});
globals: true is not mandatory, but without it expect (and describe / test)
are not on the global scope β you then have to import { expect } from "vitest" in
your step definitions, or pass a runner to loadSteps (see below).
optimizeDeps.includeβ
The browser entry pulls in @cucumber/gherkin and two jest-cucumber sub-modules
to parse .feature text. If they are not listed in optimizeDeps.include, Vite
discovers them on first import and re-optimizes, which makes Vitest print:
[vite] optimized dependencies changed. reloading
[vitest] Vite unexpectedly reloaded a test.
Tests still pass, but the reload is noisy and occasionally flaky. Listing the three modules removes it.
Setup fileβ
import.meta.glob is resolved by Vite at build time and replaces glob +
filesystem access. Pass its result to loadSteps:
// setupTests.js
import { loadSteps } from "specflow-emulator/browser";
await loadSteps({
modules: import.meta.glob("./src/**/*.stepdefinitions.js", { eager: true }),
});
Notes:
import.meta.globmust be written in your own code with a literal pattern β Vite reads it statically and it cannot be moved inside the library.- The pattern is resolved relative to the setup file, not to the project root.
{ eager: true }returns the modules directly. Without it you get lazy loaders;loadStepshandles both.- Match your step definition extension:
*.stepdefinitions.{js,ts}for a TypeScript project.
Other test runnersβ
loadSteps auto-detects Vitest. For anything else, pass the runner explicitly:
import { loadSteps } from "specflow-emulator/browser";
import { describe, test } from "my-test-runner";
await loadSteps({
runner: { describe, test },
modules: import.meta.glob("./src/**/*.stepdefinitions.js", { eager: true }),
});
Feature filesβ
With the Vite pluginβ
With specflowFeatures() registered, import the .feature file directly β the
default export is its raw Gherkin text:
// calculator.steps.js
import { defineFeature } from "specflow-emulator/browser";
import feature from "./calculator.feature";
defineFeature(feature);
Without the pluginβ
Vite handles the ?raw suffix natively, so the plugin is optional:
import { defineFeature } from "specflow-emulator/browser";
import feature from "./calculator.feature?raw";
defineFeature(feature);
Either way, defineFeature receives a string. There is no path-based form in
the browser β defineFeature("./calculator.feature") cannot work without fs.
Step definitionsβ
defineSteps is unchanged β import it from specflow-emulator/browser (it is the
same function re-exported, so specflow-emulator also works):
import { defineSteps } from "specflow-emulator/browser";
export const stepDefinitions = defineSteps(
[{ feature: "Simple Calculator", tag: "feature" }],
({ Given, When, Then }) => {
Given(/^number "(.*)"$/, (scenarioContext) => (number) => {
scenarioContext.numbers = [...(scenarioContext.numbers ?? []), number];
});
// No capture group -> the inner callback takes NO argument.
When("I add them", (scenarioContext) => () => {
scenarioContext.result = scenarioContext.numbers.reduce((a, b) => a + +b, 0);
});
Then(/^the result should be "(.*)"$/, (scenarioContext) => (expected) => {
expect(scenarioContext.result).toBe(+expected);
});
}
);
TypeScriptβ
Step definitions and .steps files can be .ts β adjust the glob and the
include pattern accordingly.
To type .feature imports, pull in the shipped ambient declarations, either with a
triple-slash reference in a .d.ts that your project already picks up (e.g.
vite-env.d.ts):
/// <reference types="specflow-emulator/feature" />
β¦or via tsconfig.json:
{
"compilerOptions": {
"types": ["vite/client", "specflow-emulator/feature"]
}
}
vite/client alone already covers the ?raw form (import x from "./y.feature?raw").
WebdriverIO providerβ
The provider is the only thing that changes:
// vitest.config.js
import { webdriverio } from "@vitest/browser-webdriverio";
export default defineConfig({
// ...same plugins / optimizeDeps / test.include ...
test: {
browser: {
enabled: true,
headless: true,
provider: webdriverio(),
instances: [{ browser: "chrome" }],
},
},
});
See the Vitest browser config for the exact provider options of your Vitest version.
Troubleshootingβ
Cannot read properties of undefined (reading 'native') (from path-scurry /
glob while loading a module) β you imported defineFeature / loadSteps from
specflow-emulator instead of specflow-emulator/browser. The Node entry pulls in
glob, which cannot be evaluated in the browser.
Module "fs" has been externalized for browser compatibility β a harmless
warning. A transitive dependency references fs at module scope; the browser code
path never calls it. Tests are unaffected.
A step times out after 5 s without any assertion running β jest-cucumber treats
the step callback as taking a done callback whenever it declares more parameters
than the step provides. If the step text has no capture group, the inner callback
must take no argument:
// β hangs: `value` is read as a done() callback
When("I submit", (ctx) => (value) => { /* ... */ });
// β
When("I submit", (ctx) => () => { /* ... */ });
Error parsing feature Gherkin on a ?raw import β make sure the file really
ends in .feature. The specflowFeatures() plugin deliberately ignores
?raw / ?url / ?inline so Vite can handle them; if both the plugin and ?raw
fire you would double-wrap the text.
No scenarios run / "No step definition has been found" β the import.meta.glob
pattern did not match. It is relative to the setup file; check the path and the
extension list.
Full exampleβ
A runnable project (Playwright + Chromium, both the plugin and the ?raw form)
lives in
examples/ecmascript-browser-vitest.