@@ -37,12 +37,12 @@ test.describe( 'data-wp-bind', () => {
37
37
38
38
test ( 'add missing checked at hydration' , async ( { page } ) => {
39
39
const el = page . getByTestId ( 'add missing checked at hydration' ) ;
40
- await expect ( el ) . toHaveAttribute ( 'checked' , '' ) ;
40
+ await expect ( el ) . toBeChecked ( ) ;
41
41
} ) ;
42
42
43
43
test ( 'remove existing checked at hydration' , async ( { page } ) => {
44
44
const el = page . getByTestId ( 'remove existing checked at hydration' ) ;
45
- await expect ( el ) . not . toHaveAttribute ( 'checked' , '' ) ;
45
+ await expect ( el ) . not . toBeChecked ( ) ;
46
46
} ) ;
47
47
48
48
test ( 'update existing checked' , async ( { page } ) => {
@@ -93,4 +93,167 @@ test.describe( 'data-wp-bind', () => {
93
93
await expect ( el ) . toHaveAttribute ( 'aria-expanded' , 'true' ) ;
94
94
await expect ( el ) . toHaveAttribute ( 'data-some-value' , 'true' ) ;
95
95
} ) ;
96
+
97
+ test . describe ( 'attribute hydration' , ( ) => {
98
+ /**
99
+ * Data structure to define a hydration test case.
100
+ */
101
+ type MatrixEntry = {
102
+ /**
103
+ * Test ID of the element (the `data-testid` attr).
104
+ */
105
+ testid : string ;
106
+ /**
107
+ * Name of the attribute being hydrated.
108
+ */
109
+ name : string ;
110
+ /**
111
+ * Array of different values to test.
112
+ */
113
+ values : Record <
114
+ /**
115
+ * The type of value we are hydrating. E.g., false is `false`,
116
+ * undef is `undefined`, emptyString is `''`, etc.
117
+ */
118
+ string ,
119
+ [
120
+ /**
121
+ * Value that the attribute should contain after hydration.
122
+ * If the attribute is missing, this value is `null`.
123
+ */
124
+ attributeValue : any ,
125
+ /**
126
+ * Value that the HTMLElement instance property should
127
+ * contain after hydration.
128
+ */
129
+ entityPropValue : any
130
+ ]
131
+ > ;
132
+ } ;
133
+
134
+ const matrix : MatrixEntry [ ] = [
135
+ {
136
+ testid : 'image' ,
137
+ name : 'width' ,
138
+ values : {
139
+ false : [ null , 5 ] ,
140
+ true : [ 'true' , 5 ] ,
141
+ null : [ null , 5 ] ,
142
+ undef : [ null , 5 ] ,
143
+ emptyString : [ '' , 5 ] ,
144
+ anyString : [ 'any' , 5 ] ,
145
+ number : [ '10' , 10 ] ,
146
+ } ,
147
+ } ,
148
+ {
149
+ testid : 'input' ,
150
+ name : 'name' ,
151
+ values : {
152
+ false : [ 'false' , 'false' ] ,
153
+ true : [ 'true' , 'true' ] ,
154
+ null : [ '' , '' ] ,
155
+ undef : [ '' , '' ] ,
156
+ emptyString : [ '' , '' ] ,
157
+ anyString : [ 'any' , 'any' ] ,
158
+ number : [ '10' , '10' ] ,
159
+ } ,
160
+ } ,
161
+ {
162
+ testid : 'input' ,
163
+ name : 'value' ,
164
+ values : {
165
+ false : [ null , 'false' ] ,
166
+ true : [ null , 'true' ] ,
167
+ null : [ null , '' ] ,
168
+ undef : [ null , '' ] ,
169
+ emptyString : [ null , '' ] ,
170
+ anyString : [ null , 'any' ] ,
171
+ number : [ null , '10' ] ,
172
+ } ,
173
+ } ,
174
+ {
175
+ testid : 'input' ,
176
+ name : 'disabled' ,
177
+ values : {
178
+ false : [ null , false ] ,
179
+ true : [ '' , true ] ,
180
+ null : [ null , false ] ,
181
+ undef : [ null , false ] ,
182
+ emptyString : [ null , false ] ,
183
+ anyString : [ '' , true ] ,
184
+ number : [ '' , true ] ,
185
+ } ,
186
+ } ,
187
+ {
188
+ testid : 'input' ,
189
+ name : 'aria-disabled' ,
190
+ values : {
191
+ false : [ 'false' , undefined ] ,
192
+ true : [ 'true' , undefined ] ,
193
+ null : [ null , undefined ] ,
194
+ undef : [ null , undefined ] ,
195
+ emptyString : [ '' , undefined ] ,
196
+ anyString : [ 'any' , undefined ] ,
197
+ number : [ '10' , undefined ] ,
198
+ } ,
199
+ } ,
200
+ ] ;
201
+
202
+ for ( const { testid, name, values } of matrix ) {
203
+ test ( `${ name } is correctly hydrated for different values` , async ( {
204
+ page,
205
+ } ) => {
206
+ for ( const type in values ) {
207
+ const [ attrValue , propValue ] = values [ type ] ;
208
+
209
+ const container = page . getByTestId ( `hydrating ${ type } ` ) ;
210
+ const el = container . getByTestId ( testid ) ;
211
+ const toggle = container . getByTestId ( 'toggle value' ) ;
212
+
213
+ const hydratedAttr = await el . getAttribute ( name ) ;
214
+ const hydratedProp = await el . evaluate (
215
+ ( node , propName ) => ( node as any ) [ propName ] ,
216
+ name
217
+ ) ;
218
+ expect ( [ type , hydratedAttr ] ) . toEqual ( [
219
+ type ,
220
+ attrValue ,
221
+ ] ) ;
222
+ expect ( [ type , hydratedProp ] ) . toEqual ( [
223
+ type ,
224
+ propValue ,
225
+ ] ) ;
226
+
227
+ // Only check the rendered value if the new value is not
228
+ // `undefined` and the attibute is neither `value` nor
229
+ // `disabled` because Preact doesn't update the attribute
230
+ // for those cases.
231
+ // See https://github.com/preactjs/preact/blob/099c38c6ef92055428afbc116d18a6b9e0c2ea2c/src/diff/index.js#L471-L494
232
+ if (
233
+ type === 'undef' &&
234
+ ( name === 'value' || name === 'undefined' )
235
+ ) {
236
+ return ;
237
+ }
238
+
239
+ await toggle . click ( { clickCount : 2 , delay : 50 } ) ;
240
+
241
+ // Values should be the same as before.
242
+ const renderedAttr = await el . getAttribute ( name ) ;
243
+ const renderedProp = await el . evaluate (
244
+ ( node , propName ) => ( node as any ) [ propName ] ,
245
+ name
246
+ ) ;
247
+ expect ( [ type , renderedAttr ] ) . toEqual ( [
248
+ type ,
249
+ attrValue ,
250
+ ] ) ;
251
+ expect ( [ type , renderedProp ] ) . toEqual ( [
252
+ type ,
253
+ propValue ,
254
+ ] ) ;
255
+ }
256
+ } ) ;
257
+ }
258
+ } ) ;
96
259
} ) ;
0 commit comments