Skip to content

Commit 79a0349

Browse files
committed
Move declaration of replacements and selectors to new function
At present the declaration of all raw un-processed selectors and their replacements is in either the class body (private vars) or the constructor. This has the effect of modifying the original selectors on the class instance vars during class instantiation, and also prevents any subclass of ExactNamedSelector or PartialNamedSelector from making changes to any replacement that has been explicitly overridden in those classes constructors. For example, it is impossible for a child class of either of these to further override the '%tagTextMatch%' replacement because those changes would either be overridden in the constructor of its parent, or all original selector replacements would not be applied. ``` class MyPartialNamedSelector extends PartialNamedSelector { public function __construct() { // Note: Calling the constructor here (before registerReplacement) // will mean that the call to registerReplacement will update the // replacement, but only for any new selector defined after the // replacement is defined. // It will not have any effect upon any selector or replacement // already registered. parent::__construct(); $this->registerReplacement('%tagTextMatch%', 'contains(text(), %locator%)'); // Note: Calling the constructor here (after registerReplacement) will // cause the above tagTextMatch replacement to be overwritten by the // override defined in the constructor of PartialNamedSelector. parent::__construct(); } } ``` Furthermore the original values of both the selectors and replacements have been mutated in the constructor so it is not possible for any child class to simply re-apply the original translations. The only solution to this problem is to extend the NamedSelector base class and manually copy the standard replacements from PartialNamedSelector or ExactNamedSelector (as appropriate) into your new child class, but this approach is not sustainable. This patch modifies the Selector classes to: * moves the storage and fetching of the original, unmodified and untranslated selectors and replacements to a function. This effectively makes them immutable; and * allows the child classes to override or define their own selectors and replacements by simply overriding the parent class function and updating or adding the required array values. This change allows the replacements (or selectors) to be updated as in the following example: ``` class MyPartialNamedSelector extends PartialNamedSelector { protected function getRawReplacements() { return array_merge(parent::getRawReplacements(), [ '%tagTextMatch%' => 'contains(text(), %locator%)', ]); } protected function getRawSelectors() { return array_merge(parent::getRawSelectors(), [ 'link' => './/a[./@href][%tagTextMatch%]', ]); } } ``` Note: For any usage where the child class was directly extending the `NamedSelector` class and defining its custom selectors and replacements in the constructor, these will need to be updated to define the relevant `getRawSelectors()` and/or `getRawReplacements()` classes as above. This change should be considered a possibly breaking change in this situation and therefore would demand a new major release (1.10.0 presumably). Whilst this is a breaking change, it will have provide a more sustainable approach in future for more advanced uses of Mink.
1 parent f7032c3 commit 79a0349

File tree

4 files changed

+110
-80
lines changed

4 files changed

+110
-80
lines changed

CHANGES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
[Unreleased]
2+
==================
3+
4+
* Move selector and replacement declaration to dedicated functions.
5+
Note: If you were previously extending the NamedSelector class and defining overrides to existing values, these will
6+
need to be moved to the relevant `getRawSelectors()` or `getRawReplacements()` function as appropriate.
7+
See #815 for further information on this change.
8+
19
1.9.0 / 2021-10-11
210
==================
311

src/Selector/ExactNamedSelector.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@
1515
*/
1616
class ExactNamedSelector extends NamedSelector
1717
{
18-
public function __construct()
18+
protected function getRawReplacements()
1919
{
20-
$this->registerReplacement('%tagTextMatch%', 'normalize-space(string(.)) = %locator%');
21-
$this->registerReplacement('%valueMatch%', './@value = %locator%');
22-
$this->registerReplacement('%titleMatch%', './@title = %locator%');
23-
$this->registerReplacement('%altMatch%', './@alt = %locator%');
24-
$this->registerReplacement('%relMatch%', './@rel = %locator%');
25-
$this->registerReplacement('%labelAttributeMatch%', './@label = %locator%');
26-
27-
parent::__construct();
20+
return array_merge(parent::getRawReplacements(), [
21+
'%tagTextMatch%' => 'normalize-space(string(.)) = %locator%',
22+
'%valueMatch%' => './@value = %locator%',
23+
'%titleMatch%' => './@title = %locator%',
24+
'%altMatch%' => './@alt = %locator%',
25+
'%relMatch%' => './@rel = %locator%',
26+
'%labelAttributeMatch%' => './@label = %locator%',
27+
]);
2828
}
2929
}

src/Selector/NamedSelector.php

Lines changed: 84 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -19,42 +19,80 @@
1919
*/
2020
class NamedSelector implements SelectorInterface
2121
{
22-
private $replacements = array(
23-
// simple replacements
24-
'%lowercaseType%' => "translate(./@type, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')",
25-
'%lowercaseRole%' => "translate(./@role, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')",
26-
'%tagTextMatch%' => 'contains(normalize-space(string(.)), %locator%)',
27-
'%labelTextMatch%' => './@id = //label[%tagTextMatch%]/@for',
28-
'%idMatch%' => './@id = %locator%',
29-
'%valueMatch%' => 'contains(./@value, %locator%)',
30-
'%idOrValueMatch%' => '(%idMatch% or %valueMatch%)',
31-
'%idOrNameMatch%' => '(%idMatch% or ./@name = %locator%)',
32-
'%placeholderMatch%' => './@placeholder = %locator%',
33-
'%titleMatch%' => 'contains(./@title, %locator%)',
34-
'%altMatch%' => 'contains(./@alt, %locator%)',
35-
'%relMatch%' => 'contains(./@rel, %locator%)',
36-
'%labelAttributeMatch%' => 'contains(./@label, %locator%)',
37-
38-
// complex replacements
39-
'%inputTypeWithoutPlaceholderFilter%' => "%lowercaseType% = 'radio' or %lowercaseType% = 'checkbox' or %lowercaseType% = 'file'",
40-
'%fieldFilterWithPlaceholder%' => 'self::input[not(%inputTypeWithoutPlaceholderFilter%)] | self::textarea',
41-
'%fieldMatchWithPlaceholder%' => '(%idOrNameMatch% or %labelTextMatch% or %placeholderMatch%)',
42-
'%fieldMatchWithoutPlaceholder%' => '(%idOrNameMatch% or %labelTextMatch%)',
43-
'%fieldFilterWithoutPlaceholder%' => 'self::input[%inputTypeWithoutPlaceholderFilter%] | self::select',
44-
'%buttonTypeFilter%' => "%lowercaseType% = 'submit' or %lowercaseType% = 'image' or %lowercaseType% = 'button' or %lowercaseType% = 'reset'",
45-
'%notFieldTypeFilter%' => "not(%buttonTypeFilter% or %lowercaseType% = 'hidden')",
46-
'%buttonMatch%' => '%idOrNameMatch% or %valueMatch% or %titleMatch%',
47-
'%linkMatch%' => '(%idMatch% or %tagTextMatch% or %titleMatch% or %relMatch%)',
48-
'%imgAltMatch%' => './/img[%altMatch%]',
49-
);
50-
51-
private $selectors = array(
52-
'fieldset' => <<<XPATH
22+
private $replacements = [];
23+
24+
private $selectors = [];
25+
26+
private $xpathEscaper;
27+
28+
/**
29+
* Creates selector instance.
30+
*/
31+
public function __construct()
32+
{
33+
$this->xpathEscaper = new Escaper();
34+
35+
foreach ($this->getRawReplacements() as $from => $to) {
36+
$this->registerReplacement($from, $to);
37+
}
38+
39+
foreach ($this->getRawSelectors() as $alias => $selector) {
40+
$this->registerNamedXpath($alias, $selector);
41+
}
42+
}
43+
44+
/**
45+
* Get the list of replacements in their raw state with replacements not applied.
46+
*
47+
* @return array
48+
*/
49+
protected function getRawReplacements()
50+
{
51+
return array(
52+
// simple replacements
53+
'%lowercaseType%' => "translate(./@type, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')",
54+
'%lowercaseRole%' => "translate(./@role, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')",
55+
'%tagTextMatch%' => 'contains(normalize-space(string(.)), %locator%)',
56+
'%labelTextMatch%' => './@id = //label[%tagTextMatch%]/@for',
57+
'%idMatch%' => './@id = %locator%',
58+
'%valueMatch%' => 'contains(./@value, %locator%)',
59+
'%idOrValueMatch%' => '(%idMatch% or %valueMatch%)',
60+
'%idOrNameMatch%' => '(%idMatch% or ./@name = %locator%)',
61+
'%placeholderMatch%' => './@placeholder = %locator%',
62+
'%titleMatch%' => 'contains(./@title, %locator%)',
63+
'%altMatch%' => 'contains(./@alt, %locator%)',
64+
'%relMatch%' => 'contains(./@rel, %locator%)',
65+
'%labelAttributeMatch%' => 'contains(./@label, %locator%)',
66+
67+
// complex replacements
68+
'%inputTypeWithoutPlaceholderFilter%' => "%lowercaseType% = 'radio' or %lowercaseType% = 'checkbox' or %lowercaseType% = 'file'",
69+
'%fieldFilterWithPlaceholder%' => 'self::input[not(%inputTypeWithoutPlaceholderFilter%)] | self::textarea',
70+
'%fieldMatchWithPlaceholder%' => '(%idOrNameMatch% or %labelTextMatch% or %placeholderMatch%)',
71+
'%fieldMatchWithoutPlaceholder%' => '(%idOrNameMatch% or %labelTextMatch%)',
72+
'%fieldFilterWithoutPlaceholder%' => 'self::input[%inputTypeWithoutPlaceholderFilter%] | self::select',
73+
'%buttonTypeFilter%' => "%lowercaseType% = 'submit' or %lowercaseType% = 'image' or %lowercaseType% = 'button' or %lowercaseType% = 'reset'",
74+
'%notFieldTypeFilter%' => "not(%buttonTypeFilter% or %lowercaseType% = 'hidden')",
75+
'%buttonMatch%' => '%idOrNameMatch% or %valueMatch% or %titleMatch%',
76+
'%linkMatch%' => '(%idMatch% or %tagTextMatch% or %titleMatch% or %relMatch%)',
77+
'%imgAltMatch%' => './/img[%altMatch%]',
78+
);
79+
}
80+
81+
/**
82+
* Get the list of selectors in their raw state with replacements not applied.
83+
*
84+
* @return array
85+
*/
86+
protected function getRawSelectors()
87+
{
88+
89+
return array(
90+
'fieldset' => <<<XPATH
5391
.//fieldset
5492
[(%idMatch% or .//legend[%tagTextMatch%])]
5593
XPATH
5694

57-
,'field' => <<<XPATH
95+
,'field' => <<<XPATH
5896
.//*
5997
[%fieldFilterWithPlaceholder%][%notFieldTypeFilter%][%fieldMatchWithPlaceholder%]
6098
|
@@ -66,15 +104,15 @@ class NamedSelector implements SelectorInterface
66104
.//label[%tagTextMatch%]//.//*[%fieldFilterWithoutPlaceholder%][%notFieldTypeFilter%]
67105
XPATH
68106

69-
,'link' => <<<XPATH
107+
,'link' => <<<XPATH
70108
.//a
71109
[./@href][(%linkMatch% or %imgAltMatch%)]
72110
|
73111
.//*
74112
[%lowercaseRole% = 'link'][(%idOrValueMatch% or %titleMatch% or %tagTextMatch%)]
75113
XPATH
76114

77-
,'button' => <<<XPATH
115+
,'button' => <<<XPATH
78116
.//input
79117
[%buttonTypeFilter%][(%buttonMatch%)]
80118
|
@@ -88,7 +126,7 @@ class NamedSelector implements SelectorInterface
88126
[%lowercaseRole% = 'button'][(%buttonMatch% or %tagTextMatch%)]
89127
XPATH
90128

91-
,'link_or_button' => <<<XPATH
129+
,'link_or_button' => <<<XPATH
92130
.//a
93131
[./@href][(%linkMatch% or %imgAltMatch%)]
94132
|
@@ -105,76 +143,60 @@ class NamedSelector implements SelectorInterface
105143
[(%lowercaseRole% = 'button' or %lowercaseRole% = 'link')][(%idOrValueMatch% or %titleMatch% or %tagTextMatch%)]
106144
XPATH
107145

108-
,'content' => <<<XPATH
146+
,'content' => <<<XPATH
109147
./descendant-or-self::*
110148
[%tagTextMatch%]
111149
XPATH
112150

113-
,'select' => <<<XPATH
151+
,'select' => <<<XPATH
114152
.//select
115153
[%fieldMatchWithoutPlaceholder%]
116154
|
117155
.//label[%tagTextMatch%]//.//select
118156
XPATH
119157

120-
,'checkbox' => <<<XPATH
158+
,'checkbox' => <<<XPATH
121159
.//input
122160
[%lowercaseType% = 'checkbox'][%fieldMatchWithoutPlaceholder%]
123161
|
124162
.//label[%tagTextMatch%]//.//input[%lowercaseType% = 'checkbox']
125163
XPATH
126164

127-
,'radio' => <<<XPATH
165+
,'radio' => <<<XPATH
128166
.//input
129167
[%lowercaseType% = 'radio'][%fieldMatchWithoutPlaceholder%]
130168
|
131169
.//label[%tagTextMatch%]//.//input[%lowercaseType% = 'radio']
132170
XPATH
133171

134-
,'file' => <<<XPATH
172+
,'file' => <<<XPATH
135173
.//input
136174
[%lowercaseType% = 'file'][%fieldMatchWithoutPlaceholder%]
137175
|
138176
.//label[%tagTextMatch%]//.//input[%lowercaseType% = 'file']
139177
XPATH
140178

141-
,'optgroup' => <<<XPATH
179+
,'optgroup' => <<<XPATH
142180
.//optgroup
143181
[%labelAttributeMatch%]
144182
XPATH
145183

146-
,'option' => <<<XPATH
184+
,'option' => <<<XPATH
147185
.//option
148186
[(./@value = %locator% or %tagTextMatch%)]
149187
XPATH
150188

151-
,'table' => <<<XPATH
189+
,'table' => <<<XPATH
152190
.//table
153191
[(%idMatch% or .//caption[%tagTextMatch%])]
154192
XPATH
155-
,'id' => <<<XPATH
193+
,'id' => <<<XPATH
156194
.//*[%idMatch%]
157195
XPATH
158-
,'id_or_name' => <<<XPATH
196+
,'id_or_name' => <<<XPATH
159197
.//*[%idOrNameMatch%]
160198
XPATH
161-
);
162-
private $xpathEscaper;
163-
164-
/**
165-
* Creates selector instance.
166-
*/
167-
public function __construct()
168-
{
169-
$this->xpathEscaper = new Escaper();
170-
171-
foreach ($this->replacements as $from => $to) {
172-
$this->registerReplacement($from, $to);
173-
}
174-
175-
foreach ($this->selectors as $alias => $selector) {
176-
$this->registerNamedXpath($alias, $selector);
177-
}
199+
);
178200
}
179201

180202
/**

src/Selector/PartialNamedSelector.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@
1717
*/
1818
class PartialNamedSelector extends NamedSelector
1919
{
20-
public function __construct()
20+
protected function getRawReplacements()
2121
{
22-
$this->registerReplacement('%tagTextMatch%', 'contains(normalize-space(string(.)), %locator%)');
23-
$this->registerReplacement('%valueMatch%', 'contains(./@value, %locator%)');
24-
$this->registerReplacement('%titleMatch%', 'contains(./@title, %locator%)');
25-
$this->registerReplacement('%altMatch%', 'contains(./@alt, %locator%)');
26-
$this->registerReplacement('%relMatch%', 'contains(./@rel, %locator%)');
27-
$this->registerReplacement('%labelAttributeMatch%', 'contains(./@label, %locator%)');
28-
29-
parent::__construct();
22+
return array_merge(parent::getRawReplacements(), [
23+
'%tagTextMatch%' => 'contains(normalize-space(string(.)), %locator%)',
24+
'%valueMatch%' => 'contains(./@value, %locator%)',
25+
'%titleMatch%' => 'contains(./@title, %locator%)',
26+
'%altMatch%' => 'contains(./@alt, %locator%)',
27+
'%relMatch%' => 'contains(./@rel, %locator%)',
28+
'%labelAttributeMatch%' => 'contains(./@label, %locator%)',
29+
]);
3030
}
3131
}

0 commit comments

Comments
 (0)