Repository navigation
Conversation
…onal bool properties
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
tyxla
left a comment
There was a problem hiding this comment.
Thanks for working on this @sdnunca!
I think the real problem is in routes/experiments-home/stage.tsx - when the setting comes back as null, the page uses {} and then saves over the whole option. This PR fixes one way to get there, but not all of them.
What do you think about not saving when the value is null? Or we could remove only the bad entries on the server, instead of making the whole setting null perhaps?
| 'properties' => $properties, | ||
| // Allow stale keys left behind by removed experiments without invalidating the entire option. | ||
| 'additionalProperties' => array( | ||
| 'type' => 'boolean', |
There was a problem hiding this comment.
This only helps when the old value can be converted to a boolean. If an old key has null or 'on', the whole setting is still null, and we still get the same bug.
| // Experiment properties must always be boolean. | ||
| 'properties' => $properties, | ||
| // Allow stale keys left behind by removed experiments without invalidating the entire option. | ||
| 'additionalProperties' => array( |
There was a problem hiding this comment.
With this change, old keys stay in the option forever, and there is no way to remove them from the UI. We have reused experiment IDs before (gutenberg-full-page-client-side-navigation), so an old true could turn on a new experiment without the admin knowing. Should we clean them up somewhere?
| // Experiment properties must always be boolean. | ||
| 'properties' => $properties, | ||
| // Allow stale keys left behind by removed experiments without invalidating the entire option. | ||
| 'additionalProperties' => array( |
There was a problem hiding this comment.
Before, the REST API returned a 400 for an unknown experiment ID. Now it saves any key. So a typo in setGutenbergExperiments() in e2e tests will not fail early anymore.
| 'type' => 'object', | ||
| 'properties' => $properties, | ||
| 'type' => 'object', | ||
| // Experiment properties must always be boolean. |
There was a problem hiding this comment.
Nit: I don't think we need this comment. This line doesn't make the properties boolean, the loop above does.
| } | ||
|
|
||
| public function tear_down() { | ||
| delete_option( 'gutenberg-experiments' ); |
There was a problem hiding this comment.
Nit: I think we don't need delete_option() here. WP_UnitTestCase already rolls back the database after each test.
| $this->assertSame( $experiments, $this->get_experiments_setting() ); | ||
| } | ||
|
|
||
| public function test_unregistered_experiments_do_not_invalidate_the_setting() { |
There was a problem hiding this comment.
This test only uses true. Can we add a case with a value like null or 'on'? Right now it still breaks the setting.
Also, this test and test_unregistered_experiments_are_preserved check almost the same thing. Maybe we can merge them into one?
| $this->assertSame( $expected, get_option( 'gutenberg-experiments' ) ); | ||
| } | ||
|
|
||
| public function test_rest_rejects_a_value_that_is_not_a_map() { |
There was a problem hiding this comment.
Small thing: an empty string ('') passes this check, because rest_is_object( '' ) returns true. Then all experiments get turned off. Maybe worth a test?
| $this->assertSame( 400, $response->get_status() ); | ||
| $this->assertSame( 'rest_invalid_param', $response->get_data()['code'] ); | ||
| $this->assertSame( $experiments, get_option( 'gutenberg-experiments' ) ); |
There was a problem hiding this comment.
Nit: we could extend WP_Test_REST_TestCase and use assertErrorResponse( 'rest_invalid_param', $response, 400 ) here
Thank you for looking at the changes @tyxla! I spent some time thinking about this and the front-end handling seems sensible to me. If we ended up with invalid data in Because From what I could tell, the experiments have always been/should always be boolean-ish , so the schema allowing unregistered booleans made sense to me. While I think the best option here would be to manage existing data when changing the experiments using a migration, I'm not sure how realistic a requirement that is. If we're open to altering the stored data, I'd say using I'll give this approach a try and get back to you. |
|
Personally, since it is an 'experimental setting' as the name suggests, I don't feel we need to worry too much about backward compatibility. I also think it would be fine to implicitly remove old keys without notifying users. Am I missing anything? |
AFAIU there's a secondary use case to address: users who routinely change Gutenberg branches back and forth may lose settings without knowing it. The most obvious scenarios concerns contributors themselves, who test different branches all day, but the most compelling scenarios to fix are those that can affect "regular users":
There are reports that this could happen in WP.com, where certain experiments are given more visibility so that users opt into them. So the safest thing right now seems to preserve unrecognised experiments and ensure they don't fail validation. To avoid accumulating stale experiments forever, maybe we could try a follow-up PR that adds a |
This seems like a good compromise. Without that, it makes me question the existence of validation in the first place. |
|
Thanks so much for the feedback, much appreciated. I've cleaned up the unit tests. AFAIU, the PR matches the compromise described above to preserve unrecognized experiments, I can tackle the |
| /** | ||
| * @dataProvider experiment_values_provider | ||
| */ | ||
| public function test_reads_experiment_values( $initial_experiments, $read_experiments ) { |
There was a problem hiding this comment.
Hmm, the data provider keys (input, output, should_fail_on_save) don't match the argument names here. It works now with the current PHPUnit version, but I believe newer PHPUnit passes string keys as named arguments, so this would break. Maybe we should use the same names in both places?
| /** | ||
| * @return array< string, array{ 'input': mixed, 'output': array|null, 'should_fail_on_save': bool }>; | ||
| */ |
There was a problem hiding this comment.
Could use a newline, and the double space and trailing ; looks unintended:
| /** | |
| * @return array< string, array{ 'input': mixed, 'output': array|null, 'should_fail_on_save': bool }>; | |
| */ | |
| /** | |
| * @return array< string, array{ 'input': mixed, 'output': array|null, 'should_fail_on_save': bool }> | |
| */ |
| $this->assertSame( 200, $response->get_status() ); | ||
| $data = $response->get_data(); | ||
| $this->assertArrayHasKey( 'gutenberg-experiments', $data ); | ||
| $this->assertSame( |
There was a problem hiding this comment.
Nit: the arguments to assertSame() are swapped. The expected value should go first, so the error message is clearer when the test fails.
| */ | ||
| public function experiment_values_provider(): array { | ||
| return array( | ||
| 'unregistered experiment, non bool, fails schema validation and returns null.' => |
There was a problem hiding this comment.
Not blocking, but none of these tests cover the original bug, where one toggle turns off all the other experiments. Maybe we can add a test for that?
There was a problem hiding this comment.
Agreed.
I'm also tempted to say that we're stretching the usefulness of @dataProvider here. Tests should be as clear to read as possible, and IMO the cleverness of factoring out experiment_values_provider is detrimental. Let's improve the setup:
- split the data into passing and failing cases:
data_valid_values,data_invalid_values,test_saves_experiments_setting,test_rejects_experiments_setting
- rename the remaining test to
test_reads_experiments_settingfor consistency - method signatures:
test_saves_experiments_setting( $input, $expected )test_rejects_experiments_setting( $input )— the expected output is alwaysnull
- dataprovider data shapes adjusted accordingly, e.g. drop the
should_fail_on_saveproperty- is it idiomatic to name the keys of each row's data? I don't remember ever seeing it. I'm tempted to suggest dropping the keys, i.e.
public function data_valid_values() {
return array(
- array( 'input' => '…', 'expected' => '…' ),
+ array( '…', '…' ),And improve the test cases themselves:
- as @tyxla points out, add cases to cover the original bug
- rename dummy value "test-experiment" to "unregistered-experiment"
- rename test cases, which are too verbose, e.g.:
- "unregistered experiment" is enough, we know from context it's a passing case
- "unregistered experiment with invalid value" is fine for the failing case
- "all experiments are returned, including bool unregistered ones." -> "registered and unregistered experiments"
- "empty string is object like." -> "empty string"
- "non-empty string fails schema validation." -> "non-legacy string value" (obvious in the context of that dataprovider)
- "object is object like." -> "empty object"
- do we need a test case with a non-empty object?
- "legacy checkbox values are coerced to bool." -> "legacy string values ('0'|'1')"
| 'type' => 'object', | ||
| 'properties' => $properties, | ||
| // Allow stale keys left behind by removed experiments without invalidating the entire option. | ||
| 'additionalProperties' => array( |
There was a problem hiding this comment.
One more thing: I think this now also accepts a list like [ true, false ], because keys 0 and 1 are counted as extra boolean properties. Before, this returned a 400. Can we check this and maybe add a test for it?

What?
Allow additional boolean properties in
gutenberg-experiments.Why?
Once a previously activated experiment is removed from the properties list, previously stored values start failing the schema validation, resulting in
gutenberg-experimentsbeing returned as null.This causes the
Experimentspage to display all toggles as OFF and clicking any of the toggles overwrites the setting value.As all properties in
gutenberg-experimentsare always boolean, the schema validation should allow additional (unregistered) boolean properties.How?
Prior art in #82072 tackles recovering from a completely invalid
gutenberg-experimentsvalue, but it ends up coercing all input to boolean.As the experiment properties should always be boolean, this PR extends the validation to allow the current registered properties + any other boolean properties.
Testing Instructions
gutenberg-experimentsto contain a removed property:wp option update gutenberg-experiments '{"gutenberg-block-experiments":true,"removed-experiment":true}' --format=jsonExperimental blockstoggle is now ON.Testing Instructions for Keyboard
Screenshots or screencast
N/A.
Use of AI Tools
No.