In Playwright, how do you use two separately extended test objects in one spec file?
answer
- Two objects, not one registry
- A test sees only its own object
- One call unions the fixture sets
- Matchers need their own merge
- mergeTests and mergeExpects
basics
~20 sCombine them with mergeTests from the Playwright test package: mergeTests(staffTest, auditTest) returns one test object carrying both fixture sets. Importing both objects side by side does not work, and mergeExpects does the same job for custom matchers.
solid answer
~40 sEach object produced by `test.extend` carries **only its own fixture set**, so a test registered on one of them cannot see the other's fixtures — importing both into a spec does not combine them. `mergeTests(staffTest, auditTest)` from `@playwright/test` returns a single `test` object that knows every fixture from both, and you can keep extending the result. Custom matchers live on `expect`, not on `test`, so a second package's matchers arrive only through `mergeExpects(staffExpect, auditExpect)`. Both inputs must be extensions of the same `@playwright/test` base. Keep fixture names distinct across packages — a clash between two independently authored sets is a confusing thing to debug. If you own both fixture sets anyway, plain chaining (`base.extend(a).extend(b)`) is simpler; merging earns its place when the packages are authored separately and neither can import the other.
code
typescript · 14 linesimport { mergeTests, mergeExpects } from '@playwright/test';
import { test as staffTest, expect as staffExpect } from './fixtures/staff';
import { test as auditTest, expect as auditExpect } from './fixtures/audit';
export const test = mergeTests(staffTest, auditTest);
export const expect = mergeExpects(staffExpect, auditExpect);
test('suspending a seat is written to the audit trail', async ({
staffSeat,
auditTrail,
}) => {
await staffSeat.suspend();
await expect(auditTrail).toContainEntry('staff.suspended');
});go deeper
Remember the name: mergeTests from the Playwright test package combines two extended test objects into one that a spec file can import.
Explain why two imports fail — a test registered on one object resolves only that object's fixtures — and that mergeExpects does the equivalent job for custom matchers.
Choose deliberately between chaining and merging: chaining is simpler and allows overrides when you own both sets, while merging suits independent packages that must not import each other.
Treat merged fixture packages as a naming problem across teams. Agree on a prefix per package so two independently shipped sets cannot collide in a spec nobody owns.
## Why two imports do not work `test.extend` does not mutate anything global. It returns a **new test object** whose fixture set is the base's plus the ones you defined. Two teams that each call `base.extend(...)` end up with two unrelated objects: ```typescript import { test as staffTest } from './fixtures/staff'; import { test as auditTest } from './fixtures/audit'; staffTest('...', async ({ staffSeat, auditTrail }) => { /* auditTrail is unknown here */ }); ``` The test is registered on `staffTest`, so `staffTest`'s fixtures are the only ones that resolve. There is no ambient registry the two could both write into, and no way for a single test to be registered on both objects. ## mergeTests ```typescript import { mergeTests } from '@playwright/test'; export const test = mergeTests(staffTest, auditTest); ``` The result is one test object whose fixture set is the union, and it behaves like any other: you can call `.extend()` on it again, and you can export it as the suite's single entry point. Practical points: - **Both inputs must extend the same `@playwright/test` base**, so a single Playwright installation. - **The result is a normal test object.** Chain more extensions onto it if the suite needs a third layer. - **It takes more than two arguments**, so a suite assembling several fixture packages merges them in one call. - **Keep names distinct.** Two packages that both define `session` produce a collision no reader will enjoy diagnosing; a short per-package prefix costs nothing. ## Custom matchers need mergeExpects Fixtures hang off `test`; custom matchers registered with `expect.extend` hang off `expect`. Merging the test objects therefore does not bring a package's matchers along: ```typescript import { mergeExpects } from '@playwright/test'; export const expect = mergeExpects(staffExpect, auditExpect); ``` Export both from one module and have every spec import `test` and `expect` from that module, so nobody has to remember which package a matcher came from. ## Merge or chain? | | Chaining `extend` | `mergeTests` | |---|---|---| | Who writes the fixtures | you own both sets | packages authored independently | | Coupling | the later layer imports the earlier one | neither package imports the other | | Layering | each layer can override the previous | union of fixture sets | | Typical home | one repo's own fixture file | a shared internal package plus a local set | Chaining is the default answer inside a single suite: `base.extend(a).extend(b)` is shorter and makes the layering explicit, and it is the only option when one layer must **override** a fixture the other defined. Merging is for the case chaining cannot express — two sets that must not depend on each other. ## In an internal admin console A console with several staff roles often grows two fixture sets from different directions: one that provisions staff seats and signs a role in, and one that captures the audit trail for assertions. They are written by different people, live in different folders, and neither should import the other. `mergeTests` gives specs a single `test` with `staffSeat` and `auditTrail` side by side, and `mergeExpects` brings the audit package's matchers with it. ## Checklist 1. Export `test` and `expect` from one module per suite. 2. Merge the fixture packages once, in that module, not in each spec. 3. Prefix fixture names per package to avoid collisions. 4. Reach for chaining first; merge when the packages must stay independent.
- When is chaining extend enough instead of mergeTests?Whenever you own both fixture sets. `base.extend(a).extend(b)` layers them in one file, keeps the order explicit, and is the only way to have a later layer override an earlier fixture. Merging earns its place when two packages are versioned separately and neither may import the other.
- What does mergeExpects do that mergeTests does not?`mergeTests` unions fixture sets on the test object. Custom matchers are registered on `expect` through `expect.extend`, so they are not part of the test object at all; `mergeExpects` combines those, and without it a merged suite has the fixtures but not the matchers.
saying these in an interview costs you the question
- Thinks importing two test objects combines their fixtures
- Believes mergeTests runs each test twice, once per object
- Expects merged test objects to bring custom matchers along
- Merges packages again inside every spec file
- Ships colliding fixture names from two separate packages