Appearance
Writing an e2e data seeder
Instructions for producing tests/furnace/e2e-data.php for an extension whose browser suite asserts on content. Written to be handed to someone — or something — that has not seen this codebase.
The contract
Two forms
A protocol suite names its seeder in furnace.json as e2e.seed, with the tokens it promises under e2e.seedTokens; that file returns a closure, furnace bootstraps Magento around it, and the browser stage receives exactly the tokens the seeder reported. The script form below is the legacy contract furnace still honours for suites without a manifest entry.
Without e2e.seed, furnace looks for, in order:
tests/furnace/e2e-data.php
tests/furnace/seed.php
dev/furnace/e2e-data.phpIt runs the first it finds inside the cell, after the integration gate and before production — so the reindex that follows makes seeded content searchable.
| variable | value |
|---|---|
FURNACE_CELL | absolute path to the Magento root to bootstrap |
FURNACE_BASE_URL | the default store's URL |
FURNACE_CELL_NAME | the cell's name, e.g. m248 |
Exit non-zero to fail the gate; the last line of output becomes the reported detail.
Rules
PHP, not REST. Store views cannot be created over the API, and a suite asserting on a translated storefront needs one. Bootstrap Magento and use its own repositories, so seeded rows go through the validation a merchant's would.
Outside app/code. Never copied into a cell as module code, never shipped to a customer.
No teardown. The next run restores the cell from its golden snapshot, which drops the database. Write it plainly.
Idempotent anyway, so it can be re-run by hand while developing specs.
Do not reuse Test/Fixture/ classes. They depend on Magento\TestFramework, which is not loaded outside the integration bootstrap.
Skeleton
php
<?php
declare(strict_types=1);
$cell = getenv('FURNACE_CELL');
if ($cell === false || !is_file($cell . '/app/bootstrap.php')) {
fwrite(STDERR, "FURNACE_CELL must name a Magento root\n");
exit(1);
}
require $cell . '/app/bootstrap.php';
$om = \Magento\Framework\App\Bootstrap::create($cell, $_SERVER)->getObjectManager();
$om->get(\Magento\Framework\App\State::class)
->setAreaCode(\Magento\Framework\App\Area::AREA_ADMINHTML);
// … create what the specs assert on …
echo "created N topics, M faqs\n";Deriving what to create
Do not guess. Read it off the suite, then confirm against a working instance.
1. Collect every URL the specs visit.
bash
grep -rhoE "goto\('[^']+'\)" --include='*.spec.ts' specs \
| sed -E "s/goto\('//;s/'\)//" | sort -uEach path is a promise about data: /faq/shipping/how-long-does-delivery-take means a topic shipping containing a FAQ with that url_key.
2. Collect every string they assert on.
bash
grep -rhoE "toContainText\('[^']{3,60}'\)|toContain\('[^']{3,60}'\)" \
--include='*.spec.ts' specs | sed -E "s/.*\('//;s/'\)//" | sort -uThese are the field values. A pair like Shipping & Delivery and Versand & Lieferung means one entity with a store-scoped override, not two.
3. Note the store codes. ?___store=de or /rest/de/V1/… requires a store view with that code. A furnace cell ships default and hyva only — anything else the seeder creates.
4. Note referenced IDs. /rest/V1/advanced-faq/items/1 and /rest/V1/advanced-faq/context/product/1 assume specific entity IDs. Seeding into a freshly restored cell makes those deterministic, but only if nothing else creates rows in those tables first.
5. Confirm against the working instance. If a developer has a site where the suite passes, read the real rows rather than inventing plausible ones:
bash
mysql <working-db> -e "SELECT * FROM qoliber_advancedfaq_topic\G"
mysql <working-db> -e "SELECT * FROM qoliber_advancedfaq_faq\G"
mysql <working-db> -e "SELECT * FROM qoliber_advancedfaq_faq_store\G"Match identifiers, url_keys, store assignments and sort orders exactly. A spec asserting on ordering will fail on data that is correct but differently sorted.
Verifying
bash
FURNACE_CELL=~/qoliber-furnace/m248 php84 tests/furnace/e2e-data.php
FURNACE_CELL=~/qoliber-furnace/m248 php84 tests/furnace/e2e-data.php # idempotentThen the suite against that cell:
bash
cd tests/playwright
BASE_URL=https://m248.test/default/ NODE_TLS_REJECT_UNAUTHORIZED=0 CI=1 \
npx playwright test --retries=0Then the real thing:
bash
furnace run --gate=all --cell=m248What is not the seeder's job
Admin authentication. Furnace creates furnace_e2e and passes ADMIN_USER, ADMIN_PASS and ADMIN_PATH.
Sample data. Cells ship Luma sample data, so joust-duffle-bag and the category tree already exist. Reference them; do not recreate them.
Making specs portable. A spec pinning baseURL to a literal is refused by furnace whatever the seeder does. That is a fix in the spec.