From 99d6de7066d4ac3b9bff4df23b5c3a849aced56c Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Thu, 7 May 2026 12:44:27 +0100 Subject: [PATCH 1/5] feat(newsletters): wire wizard SubscriptionLists to wizard-bridge events --- .../newsletters/views/settings/index.js | 164 +++++++++++++----- 1 file changed, 118 insertions(+), 46 deletions(-) diff --git a/src/wizards/newsletters/views/settings/index.js b/src/wizards/newsletters/views/settings/index.js index 1f9f2db880..1c7de43a79 100644 --- a/src/wizards/newsletters/views/settings/index.js +++ b/src/wizards/newsletters/views/settings/index.js @@ -11,10 +11,35 @@ import once from 'lodash/once'; /** * WordPress dependencies */ -import { useEffect, useState, Fragment } from '@wordpress/element'; +import { useEffect, useRef, useState, Fragment } from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; import { sprintf, __ } from '@wordpress/i18n'; -import { CheckboxControl, TextareaControl, ExternalLink, Notice } from '@wordpress/components'; +import { + CheckboxControl, + TextareaControl, + ExternalLink, + Notice, + __experimentalHStack as HStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis +} from '@wordpress/components'; + +// Wizard-bridge events. Mirror of `newspack-newsletters/src/wizard-bridge/events.js` +// — kept locally so this file is self-contained without a cross-repo import. +const NN_EVENT_NAMESPACE = 'newspack-newsletters'; +const NN_EVENTS = { + BRIDGE_MOUNTED: `${ NN_EVENT_NAMESPACE }:bridge-mounted`, + OPEN_MODAL: `${ NN_EVENT_NAMESPACE }:open-local-list-modal`, + OPEN_CONFIRM_DELETE: `${ NN_EVENT_NAMESPACE }:open-local-list-confirm-delete`, + LOCAL_LIST_SAVED: `${ NN_EVENT_NAMESPACE }:local-list-saved`, + LOCAL_LIST_DELETED: `${ NN_EVENT_NAMESPACE }:local-list-deleted`, +}; +const NN_FALLBACK_TIMEOUT_MS = 500; + +let bridgeMounted = false; +if ( typeof document !== 'undefined' ) { + document.addEventListener( NN_EVENTS.BRIDGE_MOUNTED, () => { + bridgeMounted = true; + } ); +} /** * Internal dependencies @@ -252,6 +277,8 @@ export const SubscriptionLists = ( { lockedLists, onUpdate, provider } ) => { const [ error, setError ] = useState( false ); const [ inFlight, setInFlight ] = useState( false ); const [ lists, setLists ] = useState( [] ); + const fallbackTimerRef = useRef( null ); + const updateConfig = data => { setLists( data ); if ( typeof onUpdate === 'function' ) { @@ -285,20 +312,53 @@ export const SubscriptionLists = ( { lockedLists, onUpdate, provider } ) => { newLists[ index ][ name ] = value; updateConfig( newLists ); }; - // Handle provider updates. + useEffect( () => { setError( false ); if ( provider && ! lockedLists ) { - // Empty lists before fetching to prevent previous list from appearing while fetching. setLists( [] ); fetchLists(); } }, [ provider, lockedLists ] ); + useEffect( () => { + const reload = () => fetchLists(); + document.addEventListener( NN_EVENTS.LOCAL_LIST_SAVED, reload ); + document.addEventListener( NN_EVENTS.LOCAL_LIST_DELETED, reload ); + return () => { + document.removeEventListener( NN_EVENTS.LOCAL_LIST_SAVED, reload ); + document.removeEventListener( NN_EVENTS.LOCAL_LIST_DELETED, reload ); + }; + }, [] ); + + const startFallbackTimer = fallbackUrl => { + if ( bridgeMounted || ! fallbackUrl ) { + return; + } + clearTimeout( fallbackTimerRef.current ); + fallbackTimerRef.current = setTimeout( () => { + if ( ! bridgeMounted ) { + window.location.href = fallbackUrl; + } + }, NN_FALLBACK_TIMEOUT_MS ); + }; + + const dispatchOpenAdd = () => { + document.dispatchEvent( new CustomEvent( NN_EVENTS.OPEN_MODAL, { detail: { mode: 'add' } } ) ); + startFallbackTimer( newspack_newsletters_wizard.new_subscription_lists_url ); + }; + const dispatchOpenEdit = list => { + document.dispatchEvent( new CustomEvent( NN_EVENTS.OPEN_MODAL, { detail: { mode: 'edit', list } } ) ); + startFallbackTimer( list?.edit_link ); + }; + const dispatchConfirmDelete = list => { + document.dispatchEvent( new CustomEvent( NN_EVENTS.OPEN_CONFIRM_DELETE, { detail: { list } } ) ); + startFallbackTimer( list?.edit_link ); + }; + if ( ! inFlight && ! lists?.length && ! error ) { return null; } - if ( inFlight && ! lists?.length && ! error ) { return (
@@ -325,11 +385,7 @@ export const SubscriptionLists = ( { lockedLists, onUpdate, provider } ) => { actionContent={ <> { newspack_newsletters_wizard.new_subscription_lists_url && ( - ) } @@ -342,42 +398,58 @@ export const SubscriptionLists = ( { lockedLists, onUpdate, provider } ) => { > { ! lockedLists && ! error && - lists.map( ( list, index ) => ( - { __( 'Edit', 'newspack-plugin' ) } : null - } - > - { list.active && 'local' !== list?.type && ( - <> - - - - ) } - - ) ) } + lists.map( ( list, index ) => { + const isLocal = 'local' === list?.type; + return ( + + + + + ) : list?.edit_link ? ( + { __( 'Edit', 'newspack-plugin' ) } + ) : null + } + > + { list.active && ! isLocal && ( + <> + + + + ) } + + ); + } ) } ); }; From a6bff06b2f92b3df2a9139ed5a0087b49dd1cca7 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Thu, 7 May 2026 12:47:53 +0100 Subject: [PATCH 2/5] test(newsletters): cover wizard SubscriptionLists bridge wiring --- .../newsletters/views/settings/index.test.js | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/wizards/newsletters/views/settings/index.test.js diff --git a/src/wizards/newsletters/views/settings/index.test.js b/src/wizards/newsletters/views/settings/index.test.js new file mode 100644 index 0000000000..4afc955169 --- /dev/null +++ b/src/wizards/newsletters/views/settings/index.test.js @@ -0,0 +1,79 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import apiFetch from '@wordpress/api-fetch'; + +import { SubscriptionLists } from './index'; + +jest.mock( '@wordpress/api-fetch', () => jest.fn() ); + +const NN_EVENTS = { + BRIDGE_MOUNTED: 'newspack-newsletters:bridge-mounted', + OPEN_MODAL: 'newspack-newsletters:open-local-list-modal', + OPEN_CONFIRM_DELETE: 'newspack-newsletters:open-local-list-confirm-delete', + LOCAL_LIST_SAVED: 'newspack-newsletters:local-list-saved', + LOCAL_LIST_DELETED: 'newspack-newsletters:local-list-deleted', +}; + +beforeAll( () => { + global.newspack_newsletters_wizard = { + new_subscription_lists_url: 'https://example.test/wp-admin/post-new.php?post_type=newspack_nl_list', + }; +} ); + +beforeEach( () => { + apiFetch.mockReset(); + apiFetch.mockResolvedValue( [ + { id: 'tag-1', name: 'Local A', type: 'local', active: false, db_id: 1, edit_link: 'https://example.test/edit-local-a' }, + { id: 'group-1', name: 'Remote group', type: 'group', active: true }, + ] ); + // Pretend the bridge bundle is loaded so the fallback timer doesn't navigate the test window. + document.dispatchEvent( new CustomEvent( NN_EVENTS.BRIDGE_MOUNTED ) ); +} ); + +describe( 'SubscriptionLists — wizard-bridge wiring', () => { + it( 'dispatches OPEN_MODAL with mode=add when Add New is clicked', async () => { + const listener = jest.fn(); + document.addEventListener( NN_EVENTS.OPEN_MODAL, listener ); + render( ); + await waitFor( () => expect( screen.getByRole( 'button', { name: /^Add New$/ } ) ).toBeEnabled() ); + fireEvent.click( screen.getByRole( 'button', { name: /^Add New$/ } ) ); + expect( listener ).toHaveBeenCalled(); + expect( listener.mock.calls[ 0 ][ 0 ].detail ).toEqual( { mode: 'add' } ); + document.removeEventListener( NN_EVENTS.OPEN_MODAL, listener ); + } ); + + it( 'dispatches OPEN_MODAL with mode=edit + list when Edit is clicked on a local row', async () => { + const listener = jest.fn(); + document.addEventListener( NN_EVENTS.OPEN_MODAL, listener ); + render( ); + await waitFor( () => expect( screen.getByText( 'Local A' ) ).toBeInTheDocument() ); + fireEvent.click( screen.getAllByRole( 'button', { name: /^Edit$/ } )[ 0 ] ); + expect( listener.mock.calls[ 0 ][ 0 ].detail ).toEqual( + expect.objectContaining( { mode: 'edit', list: expect.objectContaining( { db_id: 1 } ) } ) + ); + document.removeEventListener( NN_EVENTS.OPEN_MODAL, listener ); + } ); + + it( 'dispatches OPEN_CONFIRM_DELETE when Delete is clicked on a local row', async () => { + const listener = jest.fn(); + document.addEventListener( NN_EVENTS.OPEN_CONFIRM_DELETE, listener ); + render( ); + await waitFor( () => expect( screen.getByText( 'Local A' ) ).toBeInTheDocument() ); + fireEvent.click( screen.getByRole( 'button', { name: /^Delete$/ } ) ); + expect( listener.mock.calls[ 0 ][ 0 ].detail ).toEqual( expect.objectContaining( { list: expect.objectContaining( { db_id: 1 } ) } ) ); + document.removeEventListener( NN_EVENTS.OPEN_CONFIRM_DELETE, listener ); + } ); + + it( 'reloads lists when LOCAL_LIST_SAVED fires', async () => { + render( ); + await waitFor( () => expect( apiFetch ).toHaveBeenCalledTimes( 1 ) ); + document.dispatchEvent( new CustomEvent( NN_EVENTS.LOCAL_LIST_SAVED, { detail: { listId: 1, mode: 'edit' } } ) ); + await waitFor( () => expect( apiFetch ).toHaveBeenCalledTimes( 2 ) ); + } ); + + it( 'reloads lists when LOCAL_LIST_DELETED fires', async () => { + render( ); + await waitFor( () => expect( apiFetch ).toHaveBeenCalledTimes( 1 ) ); + document.dispatchEvent( new CustomEvent( NN_EVENTS.LOCAL_LIST_DELETED, { detail: { listId: 1 } } ) ); + await waitFor( () => expect( apiFetch ).toHaveBeenCalledTimes( 2 ) ); + } ); +} ); From a62ce92535851d4042bf55290ff472f728a42e31 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Thu, 7 May 2026 13:35:00 +0100 Subject: [PATCH 3/5] fix(newsletters-wizard): read bridge readiness synchronously The `bridgeMounted` flag was only set via the one-shot `newspack-newsletters:bridge-mounted` event listener. If the bridge bundle booted before this module evaluated, the event was missed and the 500ms fallback timer redirected to the legacy editor after every modal-open attempt. Replace the local mutable boolean with a sync read of `window.newspackNewslettersBridgeReady`, which the bridge now sets before dispatching the event. Refs NEWS-2152 --- .../newsletters/views/settings/index.js | 15 ++++++------- .../newsletters/views/settings/index.test.js | 22 +++++++++++++++++-- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/wizards/newsletters/views/settings/index.js b/src/wizards/newsletters/views/settings/index.js index 1c7de43a79..6fb19040c1 100644 --- a/src/wizards/newsletters/views/settings/index.js +++ b/src/wizards/newsletters/views/settings/index.js @@ -34,12 +34,11 @@ const NN_EVENTS = { }; const NN_FALLBACK_TIMEOUT_MS = 500; -let bridgeMounted = false; -if ( typeof document !== 'undefined' ) { - document.addEventListener( NN_EVENTS.BRIDGE_MOUNTED, () => { - bridgeMounted = true; - } ); -} +// Read the bridge-readiness flag synchronously rather than relying on a +// one-shot `BRIDGE_MOUNTED` event. The bridge sets the flag before +// dispatching, so listeners that register late still observe a ready +// bridge — avoiding a spurious fallback redirect. +const isBridgeReady = () => typeof window !== 'undefined' && window.newspackNewslettersBridgeReady === true; /** * Internal dependencies @@ -332,12 +331,12 @@ export const SubscriptionLists = ( { lockedLists, onUpdate, provider } ) => { }, [] ); const startFallbackTimer = fallbackUrl => { - if ( bridgeMounted || ! fallbackUrl ) { + if ( isBridgeReady() || ! fallbackUrl ) { return; } clearTimeout( fallbackTimerRef.current ); fallbackTimerRef.current = setTimeout( () => { - if ( ! bridgeMounted ) { + if ( ! isBridgeReady() ) { window.location.href = fallbackUrl; } }, NN_FALLBACK_TIMEOUT_MS ); diff --git a/src/wizards/newsletters/views/settings/index.test.js b/src/wizards/newsletters/views/settings/index.test.js index 4afc955169..5ab5dc4d44 100644 --- a/src/wizards/newsletters/views/settings/index.test.js +++ b/src/wizards/newsletters/views/settings/index.test.js @@ -25,8 +25,12 @@ beforeEach( () => { { id: 'tag-1', name: 'Local A', type: 'local', active: false, db_id: 1, edit_link: 'https://example.test/edit-local-a' }, { id: 'group-1', name: 'Remote group', type: 'group', active: true }, ] ); - // Pretend the bridge bundle is loaded so the fallback timer doesn't navigate the test window. - document.dispatchEvent( new CustomEvent( NN_EVENTS.BRIDGE_MOUNTED ) ); + // Mark the bridge ready so the fallback timer doesn't navigate the test window. + window.newspackNewslettersBridgeReady = true; +} ); + +afterEach( () => { + delete window.newspackNewslettersBridgeReady; } ); describe( 'SubscriptionLists — wizard-bridge wiring', () => { @@ -76,4 +80,18 @@ describe( 'SubscriptionLists — wizard-bridge wiring', () => { document.dispatchEvent( new CustomEvent( NN_EVENTS.LOCAL_LIST_DELETED, { detail: { listId: 1 } } ) ); await waitFor( () => expect( apiFetch ).toHaveBeenCalledTimes( 2 ) ); } ); + + it( 'does not redirect when the bridge mounted before the wizard listener registered', async () => { + // The flag is already set in beforeEach, simulating the bridge having + // completed boot before this component mounted. The fallback timer + // must NOT navigate. + jest.useFakeTimers(); + const originalHref = window.location.href; + render( ); + await waitFor( () => expect( screen.getByRole( 'button', { name: /^Add New$/ } ) ).toBeEnabled() ); + fireEvent.click( screen.getByRole( 'button', { name: /^Add New$/ } ) ); + jest.advanceTimersByTime( 600 ); + expect( window.location.href ).toBe( originalHref ); + jest.useRealTimers(); + } ); } ); From 2ea09de60c3ace296a2a51e28a43d9d7fbcd0bf8 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Thu, 7 May 2026 16:08:50 +0100 Subject: [PATCH 4/5] feat(newsletters): bundled-mode parity for ESP-list edit modal (NEWS-2168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacks the bundled-mode side onto the same wiring as NEWS-2152's local-list parity. The wizard's SubscriptionLists view dispatches OPEN_MODAL with kind='esp' for remote rows (replacing the legacy ExternalLink to the CPT editor) and the active toggle commits immediately via PATCH /lists/{db_id}. - Edit on remote rows opens the same modal in ESP mode through the wizard-bridge (legacy edit_link still serves as the fallback when the bridge isn't ready). - Inline TextControl/TextareaControl pair removed for remote rows — the modal owns title + description. - Active toggle on every row PATCHes that one row; bulk "Save Subscription Lists" button removed. - Description string surfaced under the bold ActionCard title (with the type label kept on a smaller line below) so publishers see what customisation is in place without opening the modal. --- .../newsletters/views/settings/index.js | 110 +++++++++--------- .../newsletters/views/settings/index.test.js | 47 +++++++- 2 files changed, 96 insertions(+), 61 deletions(-) diff --git a/src/wizards/newsletters/views/settings/index.js b/src/wizards/newsletters/views/settings/index.js index 6fb19040c1..df6dcec552 100644 --- a/src/wizards/newsletters/views/settings/index.js +++ b/src/wizards/newsletters/views/settings/index.js @@ -16,7 +16,6 @@ import apiFetch from '@wordpress/api-fetch'; import { sprintf, __ } from '@wordpress/i18n'; import { CheckboxControl, - TextareaControl, ExternalLink, Notice, __experimentalHStack as HStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis @@ -275,6 +274,7 @@ export const Settings = ( { export const SubscriptionLists = ( { lockedLists, onUpdate, provider } ) => { const [ error, setError ] = useState( false ); const [ inFlight, setInFlight ] = useState( false ); + const [ togglingId, setTogglingId ] = useState( null ); const [ lists, setLists ] = useState( [] ); const fallbackTimerRef = useRef( null ); @@ -294,22 +294,27 @@ export const SubscriptionLists = ( { lockedLists, onUpdate, provider } ) => { .catch( setError ) .finally( () => setInFlight( false ) ); }; - const saveLists = () => { + const handleToggleActive = async ( list, next ) => { + if ( ! list?.db_id ) { + return; + } + const snapshot = lists; + updateConfig( lists.map( row => ( row.db_id === list.db_id ? { ...row, active: next } : row ) ) ); + setTogglingId( list.db_id ); setError( false ); - setInFlight( true ); - apiFetch( { - path: '/newspack-newsletters/v1/lists', - method: 'post', - data: { lists }, - } ) - .then( updateConfig ) - .catch( setError ) - .finally( () => setInFlight( false ) ); - }; - const handleChange = ( index, name ) => value => { - const newLists = [ ...lists ]; - newLists[ index ][ name ] = value; - updateConfig( newLists ); + try { + const response = await apiFetch( { + path: `/newspack-newsletters/v1/lists/${ list.db_id }`, + method: 'PATCH', + data: { active: next }, + } ); + updateConfig( lists.map( row => ( row.db_id === list.db_id ? { ...row, ...response } : row ) ) ); + } catch ( err ) { + updateConfig( snapshot ); + setError( err ); + } finally { + setTogglingId( null ); + } }; useEffect( () => { @@ -346,8 +351,8 @@ export const SubscriptionLists = ( { lockedLists, onUpdate, provider } ) => { document.dispatchEvent( new CustomEvent( NN_EVENTS.OPEN_MODAL, { detail: { mode: 'add' } } ) ); startFallbackTimer( newspack_newsletters_wizard.new_subscription_lists_url ); }; - const dispatchOpenEdit = list => { - document.dispatchEvent( new CustomEvent( NN_EVENTS.OPEN_MODAL, { detail: { mode: 'edit', list } } ) ); + const dispatchOpenEdit = ( list, kind ) => { + document.dispatchEvent( new CustomEvent( NN_EVENTS.OPEN_MODAL, { detail: { mode: 'edit', kind, list } } ) ); startFallbackTimer( list?.edit_link ); }; const dispatchConfirmDelete = list => { @@ -382,16 +387,11 @@ export const SubscriptionLists = ( { lockedLists, onUpdate, provider } ) => { notificationLevel={ error ? 'error' : 'warning' } hasGreyHeader actionContent={ - <> - { newspack_newsletters_wizard.new_subscription_lists_url && ( - - ) } - - + ) } disabled={ inFlight || lockedLists } > @@ -399,6 +399,7 @@ export const SubscriptionLists = ( { lockedLists, onUpdate, provider } ) => { ! error && lists.map( ( list, index ) => { const isLocal = 'local' === list?.type; + const rowDisabled = inFlight || togglingId === list?.db_id; return ( { simple hasWhiteHeader title={ list.name } - description={ list?.type_label ? list.type_label : null } - disabled={ inFlight } - toggleOnChange={ handleChange( index, 'active' ) } + description={ () => ( + <> + { list.description } + { list.description && list?.type_label &&
} + { list?.type_label && ( + { list.type_label } + ) } + + ) } + disabled={ rowDisabled } + toggleOnChange={ next => handleToggleActive( list, next ) } toggleChecked={ list.active } className={ list?.id && ( list.id.startsWith( 'group' ) || list.id.startsWith( 'tag' ) ) @@ -416,37 +425,22 @@ export const SubscriptionLists = ( { lockedLists, onUpdate, provider } ) => { : '' } actionText={ - isLocal ? ( - - - + { isLocal && ( + - - ) : list?.edit_link ? ( - { __( 'Edit', 'newspack-plugin' ) } - ) : null + ) } + } - > - { list.active && ! isLocal && ( - <> - - - - ) } -
+ /> ); } ) } diff --git a/src/wizards/newsletters/views/settings/index.test.js b/src/wizards/newsletters/views/settings/index.test.js index 5ab5dc4d44..5b1c2992f9 100644 --- a/src/wizards/newsletters/views/settings/index.test.js +++ b/src/wizards/newsletters/views/settings/index.test.js @@ -23,7 +23,7 @@ beforeEach( () => { apiFetch.mockReset(); apiFetch.mockResolvedValue( [ { id: 'tag-1', name: 'Local A', type: 'local', active: false, db_id: 1, edit_link: 'https://example.test/edit-local-a' }, - { id: 'group-1', name: 'Remote group', type: 'group', active: true }, + { id: 'group-1', name: 'Remote group', type: 'group', active: true, db_id: 2, edit_link: 'https://example.test/edit-remote' }, ] ); // Mark the bridge ready so the fallback timer doesn't navigate the test window. window.newspackNewslettersBridgeReady = true; @@ -45,18 +45,59 @@ describe( 'SubscriptionLists — wizard-bridge wiring', () => { document.removeEventListener( NN_EVENTS.OPEN_MODAL, listener ); } ); - it( 'dispatches OPEN_MODAL with mode=edit + list when Edit is clicked on a local row', async () => { + it( 'dispatches OPEN_MODAL with mode=edit + kind=local when Edit is clicked on a local row', async () => { const listener = jest.fn(); document.addEventListener( NN_EVENTS.OPEN_MODAL, listener ); render( ); await waitFor( () => expect( screen.getByText( 'Local A' ) ).toBeInTheDocument() ); fireEvent.click( screen.getAllByRole( 'button', { name: /^Edit$/ } )[ 0 ] ); expect( listener.mock.calls[ 0 ][ 0 ].detail ).toEqual( - expect.objectContaining( { mode: 'edit', list: expect.objectContaining( { db_id: 1 } ) } ) + expect.objectContaining( { mode: 'edit', kind: 'local', list: expect.objectContaining( { db_id: 1 } ) } ) ); document.removeEventListener( NN_EVENTS.OPEN_MODAL, listener ); } ); + it( 'dispatches OPEN_MODAL with mode=edit + kind=esp when Edit is clicked on a remote row', async () => { + const listener = jest.fn(); + document.addEventListener( NN_EVENTS.OPEN_MODAL, listener ); + render( ); + await waitFor( () => expect( screen.getByText( 'Remote group' ) ).toBeInTheDocument() ); + // Remote rows now have an Edit button too — second one in the list. + fireEvent.click( screen.getAllByRole( 'button', { name: /^Edit$/ } )[ 1 ] ); + expect( listener.mock.calls[ 0 ][ 0 ].detail ).toEqual( + expect.objectContaining( { mode: 'edit', kind: 'esp', list: expect.objectContaining( { db_id: 2 } ) } ) + ); + document.removeEventListener( NN_EVENTS.OPEN_MODAL, listener ); + } ); + + it( 'commits the active toggle immediately via PATCH /lists/{db_id}', async () => { + render( ); + await waitFor( () => expect( screen.getByText( 'Local A' ) ).toBeInTheDocument() ); + // Configure the next response (a successful PATCH echoing the row). + apiFetch.mockResolvedValueOnce( { id: 'tag-1', db_id: 1, active: true } ); + fireEvent.click( screen.getAllByRole( 'checkbox' )[ 0 ] ); + await waitFor( () => + expect( apiFetch ).toHaveBeenLastCalledWith( { + path: '/newspack-newsletters/v1/lists/1', + method: 'PATCH', + data: { active: true }, + } ) + ); + } ); + + it( 'does not render the bulk Save Subscription Lists button', async () => { + render( ); + await waitFor( () => expect( screen.getByText( 'Local A' ) ).toBeInTheDocument() ); + expect( screen.queryByRole( 'button', { name: /Save Subscription Lists/ } ) ).not.toBeInTheDocument(); + } ); + + it( 'does not render inline title/description fields on remote rows', async () => { + render( ); + await waitFor( () => expect( screen.getByText( 'Remote group' ) ).toBeInTheDocument() ); + expect( screen.queryByLabelText( /List title/ ) ).not.toBeInTheDocument(); + expect( screen.queryByLabelText( /List description/ ) ).not.toBeInTheDocument(); + } ); + it( 'dispatches OPEN_CONFIRM_DELETE when Delete is clicked on a local row', async () => { const listener = jest.fn(); document.addEventListener( NN_EVENTS.OPEN_CONFIRM_DELETE, listener ); From 5f613d654cc32f3df83f068e03e40bb970247345 Mon Sep 17 00:00:00 2001 From: Thomas Guillot Date: Thu, 7 May 2026 17:31:56 +0100 Subject: [PATCH 5/5] chore: ignore /build output directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7aff58b919..de256cf9e2 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ Thumbs.db /vendor/ # Built files +/build /dist /packages/components/colors /packages/components/dist