Skip to content

Merge user capabilities into defaults recursively - #75

Open
aik099 wants to merge 2 commits into
minkphp:mainfrom
aik099:deep-merge-capabilities
Open

aik099 wants to merge 2 commits into
minkphp:mainfrom
aik099:deep-merge-capabilities

Conversation

@aik099

@aik099 aik099 commented Oct 11, 2026 •

Copy link
Copy Markdown
Member

User capabilities were merged into driver defaults only at the top level, so setting anything in goog:chromeOptions/ms:edgeOptions (e.g. binary or args) silently dropped all driver defaults there, including --disable-features=DeferRendererTasksAfterInput from #68.

Now:

  • maps are merged recursively
  • lists (e.g. args, excludeSwitches) are appended to
  • other values are replaced

Removing a driver default (Problem 3 from #54) is still not possible.

Solves Problem 1 of #54 issue.

🤖 Generated with Claude Code

Setting any key in `goog:chromeOptions`/`ms:edgeOptions` dropped all
driver defaults there, including the click navigation fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.34%. Comparing base (ddce443) to head (115c0f0).

Additional details and impacted files
@@             Coverage Diff              @@
##               main      #75      +/-   ##
============================================
+ Coverage     88.86%   89.34%   +0.47%     
- Complexity      197      210      +13     
============================================
  Files             1        1              
  Lines           494      516      +22     
============================================
+ Hits            439      461      +22     
  Misses           55       55              
Flag Coverage Δ
chrome 89.14% <100.00%> (+0.88%) ⬆️
chromium 88.75% <100.00%> (+0.90%) ⬆️
edge 88.75% <100.00%> (+0.50%) ⬆️
firefox 88.95% <100.00%> (+0.89%) ⬆️
selenium-2.53.1 87.98% <100.00%> (+0.93%) ⬆️
selenium-3 88.95% <100.00%> (+0.89%) ⬆️
selenium-4 88.95% <100.00%> (+0.49%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

*
* @throws \InvalidArgumentException When a list and a map are given for the same capability
*/
private function mergeCapabilities(array ...$capabilitySets): array

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was under the impression that we want to solve the problem differently (e.g. via an additional hashmap that overwrites everything when set).

I'd rather go for that than the change here because it introduces behaviour that is:

  • difficult to comprehend
  • unusual/unexpected ("merge" kinda conflicts with php's notion of array merging)
  • we're forced to keep and maintain this logic, when released

@aik099 aik099 Oct 13, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of hash map?

Right now driver per-browser default capabilities are always-on stuff that fixes problems that we don't want anyone to undo.

There would be no problem when user defines own --disable... flags because chromedriver does merge them into one comma-separated parameter when starting a browser.

Anything other you had in mind?

P.S.
I've made this PR because @stof said we need deep capability merging.

But I'm open to suggestions.

@uuf6429 uuf6429 Oct 13, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of hash map?

I guess I was thinking about the user-defined $desiredCapabilities. The current issue, (if I remember well), is that we allow the user to overwrite individual entries (or add new entries), but not erase them (array_merge() is always additive).

Right now driver per-browser default capabilities are always-on stuff that fixes problems that we don't want anyone to undo.

I got the impression that that is the issue - that we have "definitive settings that we want", but also "sane defaults that most people want, but not everyone wants".

I've made this PR because @stof said we need deep capability merging.

Maybe I'm missing the context, but to me it feels wrong to arbitrarily merge capabilities based on the array shape. Similar to the previous issue (about not being able to remove entries), we might end up with things like someone defining ["windowSize=123x456"] whereas someone else defines ["fullScreen"], ending up with a list containing both (and undefined behaviour).

@aik099 aik099 Oct 14, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @uuf6429. These examples really helped me understand the flaws of the current deep merge implementation.

I agree that a blind array shape-based merge could silently combine incompatible values (structurally valid, but impossible for the browser to understand).

What we could do is to split the browser default capabilities (WebdriverClassicDriver::DEFAULT_CAPABILITIES keys other than 'default') into 2 groups:

  1. required (always-on) ones, which are always merged last on top of user-provided ones;
  2. the rest, which user-provided ones override as a whole (shallow merge, as now).

Only the required ones would be merged into user-provided arrays (e.g. our args added to the user's goog:chromeOptions.args). Since these are the only entries we define ourselves, we don't need to know all browser/driver-specific capability shapes/combinations to prevent incompatible merge results.

This way overriding binary or enabling/disabling a Chrome feature won't make Chrome navigation fail, because --disable-features=DeferRendererTasksAfterInput would still be applied.

And, yeah, this is likely what you've meant by additional hashmap that overwrites everything initially. Only now I got this.

@stof, does this work for you as well?

Comment thread src/WebdriverClassicDriver.php Outdated
&& $this->isList($value) !== $this->isList($result[$key])
) {
throw new \InvalidArgumentException(
sprintf('Capability "%s" can\'t be merged: a list and a map were given.', $key)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this error message will be confusing for nested calls, as it reports the inner key only.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: the list/map merge error now reports the full key path, e.g. Capability "goog:chromeOptions" > "args" can't be merged: a list and a map were given.

I didn't use dots as the separator, because Firefox prefs keys contain them (e.g. browser.download.dir).

If we go with the 2 groups approach, this merge would only be used for the required capabilities, but the full key path will stay in the error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants