Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "test/generator-scope",
"title": "E2E Interactivity tests - generator scope",
"category": "text",
"icon": "heart",
"description": "",
"supports": {
"interactivity": true
},
"textdomain": "e2e-interactivity",
"viewScriptModule": "file:./view.js",
"render": "file:./render.php"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
/**
* HTML for testing scope restoration with generators.
*
* @package gutenberg-test-interactive-blocks
*/
?>

<div
data-wp-interactive="test/generator-scope"
<?php echo wp_interactivity_data_wp_context( array( 'result' => '' ) ); ?>
>
<input readonly data-wp-bind--value="context.result" data-testid="result" />
<button type="button" data-wp-on--click="callbacks.resolve" data-testid="resolve">Async resolve</button>
<button type="button" data-wp-on--click="callbacks.reject" data-testid="reject">Async reject</button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?php return array( 'dependencies' => array( '@wordpress/interactivity' ) );
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* WordPress dependencies
*/
import { store, getContext } from '@wordpress/interactivity';

store( 'test/generator-scope', {
callbacks: {
*resolve() {
try {
getContext().result = yield Promise.resolve( 'ok' );
} catch ( err ) {
getContext().result = err.toString();
}
},
*reject() {
try {
getContext().result = yield Promise.reject( new Error( '😘' ) );
} catch ( err ) {
getContext().result = err.toString();
}
},
},
} );
4 changes: 4 additions & 0 deletions packages/interactivity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Bug Fixes

- Ensure scope is restored when catching exceptions thrown in async generator actions. ([#59708](https://github.com/WordPress/gutenberg/pull/59708))

## 5.2.0 (2024-03-06)

### Bug Fixes
Expand Down
7 changes: 6 additions & 1 deletion packages/interactivity/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const handlers = {
const scope = getScope();
const gen: Generator< any > = result( ...args );

let value: any;
let value: unknown;
let it: IteratorResult< any >;

while ( true ) {
Expand All @@ -125,7 +125,12 @@ const handlers = {
try {
value = await it.value;
} catch ( e ) {
setNamespace( ns );
setScope( scope );
gen.throw( e );
} finally {
resetScope();
resetNamespace();
Comment on lines +128 to +133
Copy link
Member Author

Choose a reason for hiding this comment

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

@DAreRodz I tried 2 different approaches and they both fixed my test case. I wasn't sure why we have 2 try blocks, so my first version merge the try/finally and try/catch into one try/catch/finally. Then, since I wasn't sure what the motivation for 2 blocks were, I changed use 2 blocks. I'd like to get your insight here.

Both versions are on this branch in different commits and can be reviewed.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is the best approach: to have two separate try-catch blocks. The thing with async actions is that they can change the scope at any time―whenever they run―, thus leading to unexpected results when the scope is reset.

We decided here to set/reset the scope for synchronous code fragments, i.e., the generator code from one yield to the next one. So, from my understanding, we should reset the scope, then wait for the promise to be fulfilled (which will probably set their own scope), and if it fails, then set/reset the scope again.

}

if ( it.done ) break;
Expand Down
1 change: 1 addition & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
<!-- Exclude PHPUnit tests from file and class name sniffs (for Core parity). -->
<rule ref="WordPress.Files.FileName.NotHyphenatedLowercase">
<exclude-pattern>/phpunit/*</exclude-pattern>
<exclude-pattern>*\.asset\.php$</exclude-pattern>
Copy link
Member Author

Choose a reason for hiding this comment

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

</rule>
<rule ref="PEAR.NamingConventions.ValidClassName.Invalid">
<exclude-pattern>/phpunit/*</exclude-pattern>
Expand Down
31 changes: 31 additions & 0 deletions test/e2e/specs/interactivity/async-actions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Internal dependencies
*/
import { test, expect } from './fixtures';

test.describe( 'async actions', () => {
test.beforeAll( async ( { interactivityUtils: utils } ) => {
await utils.activatePlugins();
await utils.addPostWithBlock( 'test/generator-scope' );
} );
test.beforeEach( async ( { interactivityUtils: utils, page } ) => {
await page.goto( utils.getLink( 'test/generator-scope' ) );
} );
test.afterAll( async ( { interactivityUtils: utils } ) => {
await utils.deactivatePlugins();
await utils.deleteAllPosts();
} );

test( 'Promise generator callbacks should restore scope on resolve and reject', async ( {
page,
} ) => {
const resultInput = page.getByTestId( 'result' );
await expect( resultInput ).toHaveValue( '' );

await page.getByTestId( 'resolve' ).click();
await expect( resultInput ).toHaveValue( 'ok' );

await page.getByTestId( 'reject' ).click();
await expect( resultInput ).toHaveValue( 'Error: 😘' );
} );
} );