A Playwright Alternative for Tests That Leave the Page
A Playwright alternative that drives the real file picker with plain-language steps instead of selectors.
Automate and scale manual testing with AI ->
Above: the test below running against a public upload form, sped up with idle time removed.
You can replace Playwright scripts with tests that describe each step in plain language, including the steps that happen outside the page. TestDriver matches each step against a screenshot, so:
- you test the file picker, dialogs and other windows your users see, not only the DOM
- you don’t write or maintain selectors
- your test reads like the steps you’d give a teammate
TestDriver generates and runs black-box end-to-end tests. It starts your app in a fresh virtual machine, looks at the screen and uses a real mouse and keyboard, so it never needs selectors or access to your code. Each test is a normal Vitest file, and every run is recorded as a replay.
The example uploads a file on the-internet.herokuapp.com/upload, a public practice page, through Chrome’s real Open File dialog. The full project is on GitHub: testdriverai/example-playwright-alternative.
Playwright vs. TestDriver
Playwright uploads files with setInputFiles(), which attaches the file to the <input type="file"> element directly. The native dialog never opens:
// Playwright (illustrative, not run)
await page.goto("https://the-internet.herokuapp.com/upload");
await page.locator("#file-upload").setInputFiles("/tmp/testdriver-upload.txt");
await page.locator("#file-submit").click();
await expect(page.locator("#uploaded-files")).toHaveText("testdriver-upload.txt");
The same steps from the TestDriver test that ran:
await testdriver.find("the Choose File button in the File Uploader form").click();
await testdriver.find("the Open File dialog title at the top of the file picker window", { timeout: 15000 });
await testdriver.pressKeys(["ctrl", "l"]);
await testdriver.type("/tmp/testdriver-upload.txt");
await testdriver.pressKeys(["enter"]);
await testdriver.find("the Upload button below the Choose File button").click();
Playwright’s approach is fast and stable, and it’s the right call when you only care about what the server receives. It depends on element IDs, and it skips the dialog your users actually click through. TestDriver depends on what’s on screen, so it covers the picker too. For a broader comparison, see Playwright alternatives.
TestDriver tests are JavaScript, like Playwright Test, so the language stays the same.
Watch the full replay
The replay shows every step with its screenshot and log, including the Open File dialog.
The test
/**
* Uploads a file through Chrome's real file picker dialog on a public practice page
* and checks the page confirms the upload.
*/
import { describe, expect, it } from "vitest";
import { TestDriver } from "testdriverai/vitest/hooks";
describe("File upload (Playwright alternative)", () => {
it("uploads a file through the browser's file picker", async (context) => {
const testdriver = TestDriver(context);
// Create the file to upload inside the sandbox.
await testdriver.exec("sh", 'echo "Uploaded by a TestDriver test" > /tmp/testdriver-upload.txt', 30000);
// Open the practice upload page in Chrome.
await testdriver.provision.chrome({ url: "https://the-internet.herokuapp.com/upload" });
// Open the file picker the user would see.
await testdriver.find("the Choose File button in the File Uploader form", { timeout: 30000 }).click();
// Wait for the dialog, then type the file's path and confirm.
await testdriver.find("the Open File dialog title at the top of the file picker window", { timeout: 15000 });
await testdriver.pressKeys(["ctrl", "l"]);
await testdriver.type("/tmp/testdriver-upload.txt");
await testdriver.pressKeys(["enter"]);
// Submit the form.
await testdriver.find("the Upload button below the Choose File button").click();
// Wait for the result page, then check the file name.
await testdriver.find("the File Uploaded! heading", { timeout: 15000 });
const result = await testdriver.assert(
"the page shows the File Uploaded! heading with testdriver-upload.txt listed as the uploaded file",
);
expect(result).toBeTruthy();
// Hold the final screen so the end of the replay shows the result.
await testdriver.wait(3000);
});
});
How it works
Create the file and open the page
exec() runs a shell command in the Linux sandbox, so the test writes its own upload file first. provision.chrome() then opens the page in a real Chrome window.
Describe elements by what users see
Each find() names the label and where it sits, like “the Upload button below the Choose File button”. The Open File dialog is a separate window, not part of the page, and TestDriver finds it the same way. See locating elements.
Wait for results, then assert
find() with a timeout keeps looking until the dialog or the result page appears, so there are no fixed sleeps. assert() checks a plain-language statement against the screen and returns true or false. See making assertions.
Run it yourself
Clone the example repo, or set up TestDriver in your own project and run the test:
npx testdriverai init
npx vitest run playwright-alternative.test.mjs
To point it at your own app:
- Change the
urlinprovision.chrome()to your upload page. - Write the file you need with
exec(), or download a fixture withcurl. - Rewrite the
find()descriptions to match your labels, then add anassert()for what your app shows after the upload.
FAQ
Do I need selectors or test IDs?
No. Steps are matched on screenshots, so there are no IDs or data-testid attributes to add or keep in sync.
Can I keep my Playwright tests? Yes. Both are JavaScript test files, so they can live in the same repo. Start with the flows Playwright can’t reach, like native dialogs, and keep the rest.
Can I run it in CI? Yes. It’s a normal Vitest file and the browser runs in TestDriver’s cloud sandbox. See CI/CD.
Automate and scale manual testing with AI
TestDriver uses computer-use AI to test any app - write tests in plain English and run them anywhere.