Skip to content

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.php

It runs the first it finds inside the cell, after the integration gate and before production — so the reindex that follows makes seeded content searchable.

variablevalue
FURNACE_CELLabsolute path to the Magento root to bootstrap
FURNACE_BASE_URLthe default store's URL
FURNACE_CELL_NAMEthe 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 -u

Each 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 -u

These 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   # idempotent

Then 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=0

Then the real thing:

bash
furnace run --gate=all --cell=m248

What 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.

Furnace 1.0.0-alpha · internal tooling · qoliber.com