One click fills the whole form with realistic test data β even the custom dropdowns, date pickers, rich-text editors and file uploads other form fillers can't touch. Free, with AI built into Chrome: no API key, no account.
For QA engineers who fill the same create-form forty times a day.
Add to Chrome β it's free Β· Website and live demo
Most of them assign input.value and dispatch change. That works on a plain <input> and does nothing on the
controls modern admin panels are built from: a component-library Select is a <div> that opens a teleported
overlay, a date picker refuses typed text, a Quill editor ignores anything that isn't a real beforeinput. So you
fill the six text boxes by machine and the fourteen interesting fields by hand.
Fillsmith drives each control the way a person does β then reads it back to see whether the page kept it.
A .value = filler |
β‘ Fillsmith | |
|---|---|---|
<input>, <select>, radios |
β | β |
| Component-library dropdowns | β the component reverts the write | β opens the popup, picks a real option, waits if the list is remote |
| Date & time pickers | β text the component rejects | β clicks a day, or types in the locale's format; drives time spinners |
| Rich-text editors | β plain text, or nothing | β real markup, pasted so the editor keeps it |
| File inputs | β skipped | β
a PNG, PDF, CSV or JSON generated to match accept |
| The data | π² random, field by field | π§ one invented person: email matches name, postcode matches city |
| IBAN / VAT id / card number | β fail their checksums | β pass |
| The report | what it typed | what the control holds afterwards, and what didn't stick |
| AI | π³ cloud API, account, subscription | π§ Chrome's on-device model β optional, and never able to delay a fill |
| Your data | leaves the browser | π stays in it |
- Finds every fillable control. Native inputs,
<select>, radio groups, checkboxes, file inputs,contenteditableβ plus widgets from PrimeVue, MUI, Ant Design, react-select, Radix, Headless UI, Choices.js, Select2, Tom Select and vue-multiselect. An unknown library still works if it speaks ARIA. - Invents one coherent person. Not forty unrelated strings:
anna.becker+tv6cbv@example.combelongs to Anna Becker, who lives at a Cologne postcode and answers a Cologne phone number. A return date lands after the rental date. - Respects the control. Clamped to
min,max,step,maxlength,pattern. A dropdown only ever receives one of its own options. A required field is never left empty. - Checks its own work. Everything is read back. What a framework reverted is written again; fields that appeared because of the fill get filled too; a maximum that exists only in a validation schema is read off the error message under the field and obeyed.
- Says what went wrong. A card in the corner reports how many fields were filled, where each value came from, and which ones were left. The detail lives in the popup's Debug tab, which is still there after the card has taken itself off screen: every value and where it came from, the model's own prompts, and Open report β the last ten fills in full and the timings of the hundred around them, on a page you can read, copy, or save as one text file to attach to a ticket.
From the Chrome Web Store β one click, and it updates itself.
To run a build of your own instead, take fillsmith-<version>.zip from
Releases, unzipped, or clone this repository. Then
open chrome://extensions, turn on Developer mode, choose Load unpacked, pick the folder.
Chrome 128+, and Chrome 138+ for the on-device model β without it the rules still fill every recognised field.
Nothing to build, nothing to sign up for. The ZIP is built by CI from the tag, holds the manifest,
src/ and icons/ and nothing else, and is the same archive a store upload would get.
| How | What |
|---|---|
| Popup β Fill this page | Fills the form, then shows what went where and why |
| Popup β Dry run | Lists what it would fill and which adapter claimed it; writes nothing |
| Popup β Clear | Empties what Fillsmith filled |
Alt+Shift+F / R / C |
Fill Β· fill again as a new person Β· clear |
Alt+Shift+D |
Fill just the focused field; press again for a different value |
| Right-click β Fill this page / Fill just this field | The whole form, or the one control under the pointer |
If a shortcut does nothing (on a Mac Alt+Shift+D types Γ instead), the popup footer says which one is unassigned
and links to chrome://extensions/shortcuts.
For each field, the first step that answers wins:
- Rule β an ordered list of
[pattern, persona => value]insrc/generator.js, matched against label, name, id and placeholder.zipβ a postcode,ibanβ a valid IBAN,countryβ the persona's country tried against the dropdown's real options, under every spelling that country has. - Type default β a number, a date in the locale's format, a time, prose, markup for a rich-text editor.
- Model β everything left over goes to Chrome's built-in Gemini Nano in batches of twelve, with the persona and the page's own heading, so the answers stay coherent with each other. Each batch is written the moment it lands, not when the last one does.
- Filler β a short phrase from the persona's own vocabulary, or a seeded pick among the control's real
options. Never the field's own caption echoed back: a form full of
Alternative text 27validates nothing.
Everything derives from a seed, so a fill is reproducible: pin one in Settings β Repeat data to get the same person twice β what you want when you're reproducing a bug rather than finding one.
Two locales, EN and DE, and each is a country rather than a translation: the date format, the postcode shape, the tax and bank identifiers and the reserved phone ranges all follow it.
The fill never waits for the model. The request goes out as soon as the form has been read and writing starts immediately; a field the model owns is written the moment its answer lands, and only what's still outstanding at the end is waited for.
One npm run audit against the bundled fixture β 46 fields, 24 of them component-library widgets, three of the
dropdowns answering from a simulated server, and no on-device model in headless Chromium, so this is the rules and
the filler alone:
collect 14 ms reading the form
model 0 ms time the fill actually spent waiting for the model
firstPass 6021 ms driving 46 controls (1030 ms of it the page's own loaders)
recollect 7 ms looking again for what the fill revealed
secondPass 289 ms re-checking, and writing what it found
total 6335 ms
model is the only number a model can inflate, and it's the time the fill blocked, not the time the model took.
In the suite, a stand-in that takes 5.5 seconds a batch β 16.5 seconds over three β costs a fill zero
milliseconds: the form is complete 19 ms in, and all thirty answers still land, each written as it arrives. The
Debug tab reports both numbers so they're never confused.
Every wait in the code is a condition with a budget, never a fixed sleep: a list that arrives at once isn't paid for as though it arrived slowly.
- On-device by default. Chrome's built-in Gemini Nano runs locally β no key, no account, and after its one-time download, no network. Without it, the rules still fill every recognised field.
- Bounded, always. Loading it, downloading it and answering each have their own budget. A missing, slow or wedged model changes how good the values are, never whether they arrive.
- Warm when you get back to it. A cold
create()takes about half a minute, which no fill waits for: the first fill after Chrome has had the extension idle for a while is answered by the rules while the model comes up in the background, and the fills after it have it. The report says how long a load has been running, so a model on its way reads differently from one that isn't coming. - Your own key, if you want one. Anthropic, OpenAI or Gemini, used only for what the on-device model couldn't answer. On-device only keeps everything offline; key only skips Nano. Test this setup makes one real request and shows the provider's own answer β or its own error text.
| Permission | Why |
|---|---|
activeTab, scripting |
The filler is injected only on your action: opening the popup, Fill, a shortcut, the menu. It is not a declared content script and never runs as you browse. |
storage |
Settings, the last fill's trail, and an API key if you enter one. Local to this browser profile. |
contextMenus |
The right-click entries. |
<all_urls> |
A tester's form is on their own host, and forms are often split across origins β an embedded payment or booking frame is outside the tab's own. |
Rules and the on-device model run entirely in your browser. Anything reaches a hosted provider only if you enter an
API key, and then only for fields no rule could answer: their labels, limits and options, the form's title and a few
short texts from a table on the page (PRIVACY.md lists it all) β never what is typed into the form.
Uploaded files are generated in the page; nothing is fetched. Phone numbers come only from ranges reserved for
fiction β US 555-0100 to 555-0199, and in Germany the Bundesnetzagentur's Drama-Nummern: 030 23125 xxx,
040 66969 xxx, 089 99998 xxx, 0221 4710 xxx, 069 90009 xxx, or one of the two reserved mobile blocks β and
card numbers are the 4111 11β¦ test family.
A new rule is one line in RULES in src/generator.js β specific patterns above general
ones. Return an array to offer a dropdown several candidates:
[/\b(manufacturer|hersteller|brand|marke)\b/i, p => p.company, WEAK],WEAK makes a rule a floor rather than an answer: the field is also offered to the model, and the rule fills it
only if the model doesn't.
A new widget library is one entry in LIBS in src/adapters.js. Only root and kind are
required; the rest are hints with generic ARIA fallbacks. Make root specific enough that an application's own
wrapper class can't match it:
{
id: 'acme-select', kind
:
'choice',
root
:
'.acme-select:has(> .acme-select__trigger)',
label
:
'.acme-select__value', option
:
'[role="option"]',
overlay
:
'.acme-select__panel', filter
:
'.acme-select__search'
}
,Plain ES2020 scripts injected in the order background.js lists them; each hands its surface to
globalThis. No bundler, no framework, no runtime dependency.
src/
dom.js clicks, typing, waiting β knows nothing about forms
vocab.js word lists (generated from faker by `npm run vocab`)
generator.js seed β persona β rules β checksums
adapters.js which control is which, and what it is called
overlays.js finding, opening and closing a widget's popup
fillers.js how to drive each kind of control
uploads.js generated files for <input type="file">
hud.js the on-page progress card
content.js discovery, planning, the fill loop, repair
background.js service worker: the model, the injected file list, the record of each fill, shortcuts, menus
popup.* the toolbar popup
report.* every kept fill, on a page you can read, copy or save
welcome.html the one screen a fresh install opens
site/ the landing page; the pages workflow adds test/demo-form.html as its demo
npm install # Playwright and faker, dev only
npm test # all four suites, in parallel
npm run test:widgets # just the widget layer (fast)
npm run fixture # serve test/ at :8099: demo-form.html by hand, primevue-form.html for the hard cases, libraries-form.html for one select per library
npm run screenshots # the store pictures and the site's popup pictures
npm run site # build the landing page and its demo, and serve it at :8100
npm run typecheck # the JSDoc and types/, read over the logic files β emits nothing
npm run audit # fill the fixture with the real extension and judge the page
npm run audit -- --url URL # the same against any page you can reach
npm run release -- patch # move the version on, in both files that carry it
npm run package # the store ZIP: manifest, src and icons, nothing else
npm run icons # redraw icons/ from the mark the worker animatesTwo workflows, both real: test.yml runs the four suites on every push and pull
request, and release.yml runs them again on a v* tag, refuses one that does
not match manifest.json, then attaches the ZIP to a GitHub release with notes generated from the commits since the
last one. So a release is: move the version, land it with the change it belongs to, tag it.
npm run release -- patch # or minor, major, or an exact 1.2.3
git commit -am "Fillsmith 1.0.1: what landed"
git push origin main
git tag v1.0.1 && git push origin v1.0.1release only touches the files that carry the version β manifest.json, package.json and the lockfile's own
copy β and refuses to go backwards or over a tag that already exists. The commit subject is this project's
changelog, so it stays yours to write; the Releases page
collects them per version.
The suites judge the page, not Fillsmith's own report: test/complete.mjs presses Fill once on a clean form and
asks the page whether every required control now holds a value, and tools/audit.mjs watches the indicator, the
overlays, the console and the toolbar icon while a fill runs β a still screenshot can't tell a working progress bar
from a frozen one. CI runs all four on every push, plus a check that regenerating src/vocab.js leaves it
byte-identical.
ARCHITECTURE.md has the design, and one line per bug that ever got out β each with a regression check behind it.
MIT
