newspack-components 4.7.0-alpha.1 → 4.7.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,25 +1,16 @@
1
- # newspack-components [4.7.0-alpha.1](https://github.com/Automattic/newspack-workspace/compare/newspack-components@4.6.2...newspack-components@4.7.0-alpha.1) (2026-08-20)
1
+ # newspack-components [4.7.0-alpha.2](https://github.com/Automattic/newspack-workspace/compare/newspack-components@4.7.0-alpha.1...newspack-components@4.7.0-alpha.2) (2026-08-20)
2
2
 
3
3
 
4
4
  ### Bug Fixes
5
5
 
6
- * **audience:** clearer schedule, impact preview and deal id controls ([#798](https://github.com/Automattic/newspack-workspace/issues/798)) ([4fbc1c8](https://github.com/Automattic/newspack-workspace/commit/4fbc1c8edfad6a78892ef1b8979b9113c70a45a7))
7
- * **audience:** consistent headers, goal-first rules, accessible controls ([#757](https://github.com/Automattic/newspack-workspace/issues/757)) ([345764c](https://github.com/Automattic/newspack-workspace/commit/345764ca6ead1c5e306515b456106c65ebbaaf5d))
8
- * **audience:** pricing tables in cards, price editing in a drawer ([#826](https://github.com/Automattic/newspack-workspace/issues/826)) ([8424556](https://github.com/Automattic/newspack-workspace/commit/8424556322f3fbbdc4880417c77437119353d3aa))
9
- * three WordPress 7.1 regressions in Newsletters ([#876](https://github.com/Automattic/newspack-workspace/issues/876)) ([2cbc687](https://github.com/Automattic/newspack-workspace/commit/2cbc6877394b03eb1fff4694753414ef37d03406))
6
+ * **wizard:** refetch data after the installer adds required plugins ([#893](https://github.com/Automattic/newspack-workspace/issues/893)) ([6b23902](https://github.com/Automattic/newspack-workspace/commit/6b239026e4a3dad5118443d9bc384d53248f38fa))
10
7
 
11
-
12
- ### Features
13
-
14
- * **access-control:** make Audience Management an explicit prerequisite (NPPD-1846) ([#765](https://github.com/Automattic/newspack-workspace/issues/765)) ([b5efde2](https://github.com/Automattic/newspack-workspace/commit/b5efde2aa3d050bda9fc629f1ed91e4864a91d33))
15
- * **campaigns:** Contextual Prompts, AI-assisted story donation asks ([#686](https://github.com/Automattic/newspack-workspace/issues/686)) ([f896387](https://github.com/Automattic/newspack-workspace/commit/f896387b5c3c096b6ed0cacbf2fe89b3e7f6b703)), closes [#719](https://github.com/Automattic/newspack-workspace/issues/719)
16
- * **campaigns:** pick donor landing page via autocomplete search ([#761](https://github.com/Automattic/newspack-workspace/issues/761)) ([01fa615](https://github.com/Automattic/newspack-workspace/commit/01fa61514954742094dff2fd0e5a4ff3bb4c04b6))
17
- * **components:** add a drawer component with a compound slot api ([#810](https://github.com/Automattic/newspack-workspace/issues/810)) ([5134ae9](https://github.com/Automattic/newspack-workspace/commit/5134ae9cab353bc8e10e239f7d54f8e3abb24fae))
8
+ ## newspack-components [4.6.3](https://github.com/Automattic/newspack-workspace/compare/newspack-components@4.6.2...newspack-components@4.6.3) (2026-08-20)
18
9
 
19
10
 
20
- ### Dependencies
11
+ ### Bug Fixes
21
12
 
22
- * **newspack-icons:** upgraded to 1.1.2-alpha.1
13
+ * **wizard:** refetch data after the installer adds required plugins ([#893](https://github.com/Automattic/newspack-workspace/issues/893)) ([6b23902](https://github.com/Automattic/newspack-workspace/commit/6b239026e4a3dad5118443d9bc384d53248f38fa))
23
14
 
24
15
  ## newspack-components [4.6.2](https://github.com/Automattic/newspack-workspace/compare/newspack-components@4.6.1...newspack-components@4.6.2) (2026-08-17)
25
16
 
@@ -197,6 +197,8 @@ var Wizard = function Wizard(_ref2, ref) {
197
197
  var notices = (0, _data.useSelect)(function (select) {
198
198
  return select(_store.WIZARD_STORE_NAMESPACE).getNotices();
199
199
  });
200
+ var _useDispatch2 = (0, _data.useDispatch)(_store.WIZARD_STORE_NAMESPACE),
201
+ invalidateResolution = _useDispatch2.invalidateResolution;
200
202
  var actions = headerData.actions,
201
203
  backNav = headerData.backNav,
202
204
  badges = headerData.badges,
@@ -225,6 +227,30 @@ var Wizard = function Wizard(_ref2, ref) {
225
227
  _useState2 = (0, _slicedToArray2["default"])(_useState, 2),
226
228
  pluginRequirementsSatisfied = _useState2[0],
227
229
  setPluginRequirementsSatisfied = _useState2[1];
230
+
231
+ // The data fetch above runs once per mount, while the required plugins are
232
+ // still missing — endpoints that need them answer empty or error outright,
233
+ // and either way the resolver records that as the answer. So the installer
234
+ // has to trigger a refetch, or the section mounts against it and renders
235
+ // nothing until the user saves. Requirements already met on mount are left
236
+ // alone: that fetch saw the real site, and refetching would cost every such
237
+ // wizard a second request on every load.
238
+ var requirementsWereUnmet = (0, _element.useRef)(false);
239
+ var onPluginStatus = function onPluginStatus(_ref3) {
240
+ var complete = _ref3.complete;
241
+ // Leave the installer first: whatever happens to the refetch, the user
242
+ // should not be stranded on a required-plugins screen for a plugin they
243
+ // just installed.
244
+ setPluginRequirementsSatisfied(complete);
245
+ if (!complete) {
246
+ requirementsWereUnmet.current = true;
247
+ } else if (requirementsWereUnmet.current) {
248
+ requirementsWereUnmet.current = false;
249
+ if (apiSlug && isInitialFetchTriggered) {
250
+ invalidateResolution('getWizardAPIData', [apiSlug]);
251
+ }
252
+ }
253
+ };
228
254
  if (!pluginRequirementsSatisfied) {
229
255
  headerText = requiredPlugins.length > 1 ? (0, _i18n.__)('Required plugins', 'newspack-plugin') : (0, _i18n.__)('Required plugin', 'newspack-plugin');
230
256
  displayedSections = [{
@@ -232,10 +258,7 @@ var Wizard = function Wizard(_ref2, ref) {
232
258
  render: function render() {
233
259
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_.PluginInstaller, {
234
260
  plugins: requiredPlugins,
235
- onStatus: function onStatus(_ref3) {
236
- var complete = _ref3.complete;
237
- return setPluginRequirementsSatisfied(complete);
238
- }
261
+ onStatus: onPluginStatus
239
262
  });
240
263
  }
241
264
  }];
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
5
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
6
+ var _react = require("@testing-library/react");
7
+ var _apiFetch = _interopRequireDefault(require("@wordpress/api-fetch"));
8
+ var _ = _interopRequireDefault(require("./"));
9
+ var _utils = require("./store/utils");
10
+ var _jsxRuntime = require("react/jsx-runtime");
11
+ function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine2(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function d(t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i["return"]) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine2(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine2(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine2(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine2(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine2(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine2(u), _regeneratorDefine2(u, o, "Generator"), _regeneratorDefine2(u, n, function () { return this; }), _regeneratorDefine2(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function _regenerator() { return { w: i, m: f }; })(); }
12
+ function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine2 = function _regeneratorDefine(e, r, n, t) { function o(r, n) { _regeneratorDefine2(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine2(e, r, n, t); } /**
13
+ * External dependencies.
14
+ */ /**
15
+ * WordPress dependencies.
16
+ */ /**
17
+ * Internal dependencies.
18
+ */
19
+ jest.mock('@wordpress/api-fetch', function () {
20
+ return jest.fn();
21
+ });
22
+
23
+ // Both globals are localized onto every real wizard screen. The footer's
24
+ // ExternalLink needs a string href, and the debug Notice reads aux data.
25
+ window.newspack_aux_data = {
26
+ is_debug_mode: false
27
+ };
28
+ window.newspack_urls = {
29
+ support: 'https://help.newspack.com/'
30
+ };
31
+ var SETTINGS = {
32
+ minimumDonation: '5'
33
+ };
34
+
35
+ // Stands in for a wizard section that can only render once its API data has
36
+ // arrived — the shape a wizard section has when the wizard owns an apiSlug.
37
+ var Section = function Section(_ref) {
38
+ var slug = _ref.slug;
39
+ var wizardData = (0, _utils.useWizardData)(slug);
40
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
41
+ children: wizardData.settings ? 'Settings form' : null
42
+ });
43
+ };
44
+
45
+ // The other shape: a section on a wizard with no apiSlug, which reads page-load
46
+ // globals rather than wizard API data and so never touches the store.
47
+ var StaticSection = function StaticSection() {
48
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
49
+ children: "Static section"
50
+ });
51
+ };
52
+
53
+ // Mocks the endpoints a wizard hits, gated on whether the required plugin is
54
+ // active: the wizard's own endpoint 400s while the plugin is missing, exactly as
55
+ // Audience_Donations::api_get_donation_settings() does without WooCommerce.
56
+ // Counts every wizard-endpoint GET, whatever the slug, so a test can assert that
57
+ // a wizard given no apiSlug fetches nothing at all.
58
+ var mockEndpoints = function mockEndpoints(slug) {
59
+ var initialStatus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'inactive';
60
+ var counts = {
61
+ wizard: 0
62
+ };
63
+ var pluginStatus = initialStatus;
64
+ var plugin = function plugin() {
65
+ return {
66
+ Name: 'WooCommerce',
67
+ Description: 'Store',
68
+ Status: pluginStatus,
69
+ Download: 'wporg'
70
+ };
71
+ };
72
+ _apiFetch["default"].mockImplementation(function (_ref2) {
73
+ var path = _ref2.path,
74
+ method = _ref2.method;
75
+ if (path.startsWith('/newspack/v1/wizard/') && 'POST' !== method) {
76
+ counts.wizard++;
77
+ return 'active' === pluginStatus ? Promise.resolve({
78
+ settings: SETTINGS
79
+ }) : Promise.reject({
80
+ code: 'newspack_missing_required_plugin',
81
+ message: 'The WooCommerce plugin is not installed and activated.'
82
+ });
83
+ }
84
+ if (path === '/newspack/v1/plugins/') {
85
+ return Promise.resolve({
86
+ woocommerce: plugin()
87
+ });
88
+ }
89
+ if (path === '/newspack/v1/plugins/woocommerce/configure/') {
90
+ pluginStatus = 'active';
91
+ return Promise.resolve(plugin());
92
+ }
93
+ return Promise.resolve({});
94
+ });
95
+ return counts;
96
+ };
97
+ var renderWizard = function renderWizard(slug) {
98
+ var _ref3 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
99
+ _ref3$withApiSlug = _ref3.withApiSlug,
100
+ withApiSlug = _ref3$withApiSlug === void 0 ? true : _ref3$withApiSlug;
101
+ return (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
102
+ headerText: "Test wizard",
103
+ apiSlug: withApiSlug ? slug : undefined,
104
+ requiredPlugins: ['woocommerce'],
105
+ sections: [{
106
+ label: 'Configuration',
107
+ path: '/configuration',
108
+ render: function render() {
109
+ return withApiSlug ? /*#__PURE__*/(0, _jsxRuntime.jsx)(Section, {
110
+ slug: slug
111
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(StaticSection, {});
112
+ }
113
+ }]
114
+ }));
115
+ };
116
+
117
+ // The plugin's own row button, not the footer's activate-all — both read
118
+ // "Activate", and the row button comes first in the DOM.
119
+ var clickActivate = /*#__PURE__*/function () {
120
+ var _ref4 = (0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee() {
121
+ var _yield$screen$findAll, _yield$screen$findAll2, activate;
122
+ return _regenerator().w(function (_context) {
123
+ while (1) switch (_context.n) {
124
+ case 0:
125
+ _context.n = 1;
126
+ return _react.screen.findAllByRole('button', {
127
+ name: 'Activate'
128
+ });
129
+ case 1:
130
+ _yield$screen$findAll = _context.v;
131
+ _yield$screen$findAll2 = (0, _slicedToArray2["default"])(_yield$screen$findAll, 1);
132
+ activate = _yield$screen$findAll2[0];
133
+ _react.fireEvent.click(activate);
134
+ case 2:
135
+ return _context.a(2);
136
+ }
137
+ }, _callee);
138
+ }));
139
+ return function clickActivate() {
140
+ return _ref4.apply(this, arguments);
141
+ };
142
+ }();
143
+
144
+ // Lets every already-queued promise settle, so an assertion that something did
145
+ // NOT happen has actually given it the chance to.
146
+ var flushPending = function flushPending() {
147
+ return (0, _react.act)(function () {
148
+ return new Promise(function (resolve) {
149
+ return setTimeout(resolve, 0);
150
+ });
151
+ });
152
+ };
153
+ describe('Wizard', function () {
154
+ beforeEach(function () {
155
+ _apiFetch["default"].mockReset();
156
+ });
157
+ it('refetches its API data after the installer satisfies the plugin requirements', /*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee2() {
158
+ var slug, counts, _t, _t2;
159
+ return _regenerator().w(function (_context2) {
160
+ while (1) switch (_context2.n) {
161
+ case 0:
162
+ // A distinct slug per test: the store's resolution cache lives in the
163
+ // module registry, so it is shared by every test in this file.
164
+ slug = 'test-wizard-refetch';
165
+ counts = mockEndpoints(slug);
166
+ renderWizard(slug);
167
+
168
+ // The first fetch runs before the plugin exists, so it fails. Until the
169
+ // installer is satisfied it owns the whole route, so the installer screen
170
+ // showing is what proves we are in the pre-activation state.
171
+ _context2.n = 1;
172
+ return (0, _react.waitFor)(function () {
173
+ return expect(counts.wizard).toBe(1);
174
+ });
175
+ case 1:
176
+ _t = expect;
177
+ _context2.n = 2;
178
+ return _react.screen.findByText('WooCommerce');
179
+ case 2:
180
+ _t(_context2.v).toBeInTheDocument();
181
+ _context2.n = 3;
182
+ return clickActivate();
183
+ case 3:
184
+ _t2 = expect;
185
+ _context2.n = 4;
186
+ return _react.screen.findByText('Settings form');
187
+ case 4:
188
+ _t2(_context2.v).toBeInTheDocument();
189
+ _context2.n = 5;
190
+ return flushPending();
191
+ case 5:
192
+ expect(counts.wizard).toBe(2);
193
+ case 6:
194
+ return _context2.a(2);
195
+ }
196
+ }, _callee2);
197
+ })));
198
+ it('does not refetch when the required plugins were already active', /*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee3() {
199
+ var slug, counts, _t3;
200
+ return _regenerator().w(function (_context3) {
201
+ while (1) switch (_context3.n) {
202
+ case 0:
203
+ slug = 'test-wizard-no-refetch';
204
+ counts = mockEndpoints(slug, 'active');
205
+ renderWizard(slug);
206
+ _t3 = expect;
207
+ _context3.n = 1;
208
+ return _react.screen.findByText('Settings form');
209
+ case 1:
210
+ _t3(_context3.v).toBeInTheDocument();
211
+ _context3.n = 2;
212
+ return flushPending();
213
+ case 2:
214
+ expect(counts.wizard).toBe(1);
215
+ case 3:
216
+ return _context3.a(2);
217
+ }
218
+ }, _callee3);
219
+ })));
220
+ it('fetches nothing for a wizard that has required plugins but no API slug', /*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee4() {
221
+ var slug, counts, _t4;
222
+ return _regenerator().w(function (_context4) {
223
+ while (1) switch (_context4.n) {
224
+ case 0:
225
+ // The shape of every other Newspack wizard that gates on plugins: it
226
+ // reads page-load globals rather than wizard API data. This change has to
227
+ // stay inert for them. Note this pins the inertness, not the `apiSlug`
228
+ // guard itself — the resolver already no-ops on an undefined slug, so
229
+ // that guard is a second line of defence rather than the load-bearing one.
230
+ slug = 'test-wizard-no-api-slug';
231
+ counts = mockEndpoints(slug);
232
+ renderWizard(slug, {
233
+ withApiSlug: false
234
+ });
235
+ _context4.n = 1;
236
+ return clickActivate();
237
+ case 1:
238
+ _t4 = expect;
239
+ _context4.n = 2;
240
+ return _react.screen.findByText('Static section');
241
+ case 2:
242
+ _t4(_context4.v).toBeInTheDocument();
243
+ _context4.n = 3;
244
+ return flushPending();
245
+ case 3:
246
+ expect(counts.wizard).toBe(0);
247
+ case 4:
248
+ return _context4.a(2);
249
+ }
250
+ }, _callee4);
251
+ })));
252
+ });
@@ -13,7 +13,7 @@ import classnames from 'classnames';
13
13
  // Notice is aliased: `Notice` below is Newspack's own, which this file also uses.
14
14
  import { DropdownMenu, MenuGroup, MenuItem, Notice as CoreNotice, SlotFillProvider, createSlotFill } from '@wordpress/components';
15
15
  import { useDispatch, useSelect } from '@wordpress/data';
16
- import { cloneElement, createInterpolateElement, isValidElement, useEffect, useState, forwardRef } from '@wordpress/element';
16
+ import { cloneElement, createInterpolateElement, isValidElement, useRef, useEffect, useState, forwardRef } from '@wordpress/element';
17
17
  import { __ } from '@wordpress/i18n';
18
18
  import { category, chevronLeft, moreVertical } from '@wordpress/icons';
19
19
 
@@ -196,6 +196,8 @@ var Wizard = function Wizard(_ref2, ref) {
196
196
  var notices = useSelect(function (select) {
197
197
  return select(WIZARD_STORE_NAMESPACE).getNotices();
198
198
  });
199
+ var _useDispatch2 = useDispatch(WIZARD_STORE_NAMESPACE),
200
+ invalidateResolution = _useDispatch2.invalidateResolution;
199
201
  var actions = headerData.actions,
200
202
  backNav = headerData.backNav,
201
203
  badges = headerData.badges,
@@ -224,6 +226,30 @@ var Wizard = function Wizard(_ref2, ref) {
224
226
  _useState2 = _slicedToArray(_useState, 2),
225
227
  pluginRequirementsSatisfied = _useState2[0],
226
228
  setPluginRequirementsSatisfied = _useState2[1];
229
+
230
+ // The data fetch above runs once per mount, while the required plugins are
231
+ // still missing — endpoints that need them answer empty or error outright,
232
+ // and either way the resolver records that as the answer. So the installer
233
+ // has to trigger a refetch, or the section mounts against it and renders
234
+ // nothing until the user saves. Requirements already met on mount are left
235
+ // alone: that fetch saw the real site, and refetching would cost every such
236
+ // wizard a second request on every load.
237
+ var requirementsWereUnmet = useRef(false);
238
+ var onPluginStatus = function onPluginStatus(_ref3) {
239
+ var complete = _ref3.complete;
240
+ // Leave the installer first: whatever happens to the refetch, the user
241
+ // should not be stranded on a required-plugins screen for a plugin they
242
+ // just installed.
243
+ setPluginRequirementsSatisfied(complete);
244
+ if (!complete) {
245
+ requirementsWereUnmet.current = true;
246
+ } else if (requirementsWereUnmet.current) {
247
+ requirementsWereUnmet.current = false;
248
+ if (apiSlug && isInitialFetchTriggered) {
249
+ invalidateResolution('getWizardAPIData', [apiSlug]);
250
+ }
251
+ }
252
+ };
227
253
  if (!pluginRequirementsSatisfied) {
228
254
  headerText = requiredPlugins.length > 1 ? __('Required plugins', 'newspack-plugin') : __('Required plugin', 'newspack-plugin');
229
255
  displayedSections = [{
@@ -231,10 +257,7 @@ var Wizard = function Wizard(_ref2, ref) {
231
257
  render: function render() {
232
258
  return /*#__PURE__*/_jsx(PluginInstaller, {
233
259
  plugins: requiredPlugins,
234
- onStatus: function onStatus(_ref3) {
235
- var complete = _ref3.complete;
236
- return setPluginRequirementsSatisfied(complete);
237
- }
260
+ onStatus: onPluginStatus
238
261
  });
239
262
  }
240
263
  }];
@@ -0,0 +1,254 @@
1
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
+ import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
3
+ function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine2(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function d(t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i["return"]) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine2(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine2(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine2(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine2(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine2(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine2(u), _regeneratorDefine2(u, o, "Generator"), _regeneratorDefine2(u, n, function () { return this; }), _regeneratorDefine2(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function _regenerator() { return { w: i, m: f }; })(); }
4
+ function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine2 = function _regeneratorDefine(e, r, n, t) { function o(r, n) { _regeneratorDefine2(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine2(e, r, n, t); }
5
+ /**
6
+ * External dependencies.
7
+ */
8
+ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
9
+
10
+ /**
11
+ * WordPress dependencies.
12
+ */
13
+ import apiFetch from '@wordpress/api-fetch';
14
+
15
+ /**
16
+ * Internal dependencies.
17
+ */
18
+ import Wizard from './';
19
+ import { useWizardData } from './store/utils';
20
+ import { jsx as _jsx } from "react/jsx-runtime";
21
+ jest.mock('@wordpress/api-fetch', function () {
22
+ return jest.fn();
23
+ });
24
+
25
+ // Both globals are localized onto every real wizard screen. The footer's
26
+ // ExternalLink needs a string href, and the debug Notice reads aux data.
27
+ window.newspack_aux_data = {
28
+ is_debug_mode: false
29
+ };
30
+ window.newspack_urls = {
31
+ support: 'https://help.newspack.com/'
32
+ };
33
+ var SETTINGS = {
34
+ minimumDonation: '5'
35
+ };
36
+
37
+ // Stands in for a wizard section that can only render once its API data has
38
+ // arrived — the shape a wizard section has when the wizard owns an apiSlug.
39
+ var Section = function Section(_ref) {
40
+ var slug = _ref.slug;
41
+ var wizardData = useWizardData(slug);
42
+ return /*#__PURE__*/_jsx("div", {
43
+ children: wizardData.settings ? 'Settings form' : null
44
+ });
45
+ };
46
+
47
+ // The other shape: a section on a wizard with no apiSlug, which reads page-load
48
+ // globals rather than wizard API data and so never touches the store.
49
+ var StaticSection = function StaticSection() {
50
+ return /*#__PURE__*/_jsx("div", {
51
+ children: "Static section"
52
+ });
53
+ };
54
+
55
+ // Mocks the endpoints a wizard hits, gated on whether the required plugin is
56
+ // active: the wizard's own endpoint 400s while the plugin is missing, exactly as
57
+ // Audience_Donations::api_get_donation_settings() does without WooCommerce.
58
+ // Counts every wizard-endpoint GET, whatever the slug, so a test can assert that
59
+ // a wizard given no apiSlug fetches nothing at all.
60
+ var mockEndpoints = function mockEndpoints(slug) {
61
+ var initialStatus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'inactive';
62
+ var counts = {
63
+ wizard: 0
64
+ };
65
+ var pluginStatus = initialStatus;
66
+ var plugin = function plugin() {
67
+ return {
68
+ Name: 'WooCommerce',
69
+ Description: 'Store',
70
+ Status: pluginStatus,
71
+ Download: 'wporg'
72
+ };
73
+ };
74
+ apiFetch.mockImplementation(function (_ref2) {
75
+ var path = _ref2.path,
76
+ method = _ref2.method;
77
+ if (path.startsWith('/newspack/v1/wizard/') && 'POST' !== method) {
78
+ counts.wizard++;
79
+ return 'active' === pluginStatus ? Promise.resolve({
80
+ settings: SETTINGS
81
+ }) : Promise.reject({
82
+ code: 'newspack_missing_required_plugin',
83
+ message: 'The WooCommerce plugin is not installed and activated.'
84
+ });
85
+ }
86
+ if (path === '/newspack/v1/plugins/') {
87
+ return Promise.resolve({
88
+ woocommerce: plugin()
89
+ });
90
+ }
91
+ if (path === '/newspack/v1/plugins/woocommerce/configure/') {
92
+ pluginStatus = 'active';
93
+ return Promise.resolve(plugin());
94
+ }
95
+ return Promise.resolve({});
96
+ });
97
+ return counts;
98
+ };
99
+ var renderWizard = function renderWizard(slug) {
100
+ var _ref3 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
101
+ _ref3$withApiSlug = _ref3.withApiSlug,
102
+ withApiSlug = _ref3$withApiSlug === void 0 ? true : _ref3$withApiSlug;
103
+ return render(/*#__PURE__*/_jsx(Wizard, {
104
+ headerText: "Test wizard",
105
+ apiSlug: withApiSlug ? slug : undefined,
106
+ requiredPlugins: ['woocommerce'],
107
+ sections: [{
108
+ label: 'Configuration',
109
+ path: '/configuration',
110
+ render: function render() {
111
+ return withApiSlug ? /*#__PURE__*/_jsx(Section, {
112
+ slug: slug
113
+ }) : /*#__PURE__*/_jsx(StaticSection, {});
114
+ }
115
+ }]
116
+ }));
117
+ };
118
+
119
+ // The plugin's own row button, not the footer's activate-all — both read
120
+ // "Activate", and the row button comes first in the DOM.
121
+ var clickActivate = /*#__PURE__*/function () {
122
+ var _ref4 = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
123
+ var _yield$screen$findAll, _yield$screen$findAll2, activate;
124
+ return _regenerator().w(function (_context) {
125
+ while (1) switch (_context.n) {
126
+ case 0:
127
+ _context.n = 1;
128
+ return screen.findAllByRole('button', {
129
+ name: 'Activate'
130
+ });
131
+ case 1:
132
+ _yield$screen$findAll = _context.v;
133
+ _yield$screen$findAll2 = _slicedToArray(_yield$screen$findAll, 1);
134
+ activate = _yield$screen$findAll2[0];
135
+ fireEvent.click(activate);
136
+ case 2:
137
+ return _context.a(2);
138
+ }
139
+ }, _callee);
140
+ }));
141
+ return function clickActivate() {
142
+ return _ref4.apply(this, arguments);
143
+ };
144
+ }();
145
+
146
+ // Lets every already-queued promise settle, so an assertion that something did
147
+ // NOT happen has actually given it the chance to.
148
+ var flushPending = function flushPending() {
149
+ return act(function () {
150
+ return new Promise(function (resolve) {
151
+ return setTimeout(resolve, 0);
152
+ });
153
+ });
154
+ };
155
+ describe('Wizard', function () {
156
+ beforeEach(function () {
157
+ apiFetch.mockReset();
158
+ });
159
+ it('refetches its API data after the installer satisfies the plugin requirements', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
160
+ var slug, counts, _t, _t2;
161
+ return _regenerator().w(function (_context2) {
162
+ while (1) switch (_context2.n) {
163
+ case 0:
164
+ // A distinct slug per test: the store's resolution cache lives in the
165
+ // module registry, so it is shared by every test in this file.
166
+ slug = 'test-wizard-refetch';
167
+ counts = mockEndpoints(slug);
168
+ renderWizard(slug);
169
+
170
+ // The first fetch runs before the plugin exists, so it fails. Until the
171
+ // installer is satisfied it owns the whole route, so the installer screen
172
+ // showing is what proves we are in the pre-activation state.
173
+ _context2.n = 1;
174
+ return waitFor(function () {
175
+ return expect(counts.wizard).toBe(1);
176
+ });
177
+ case 1:
178
+ _t = expect;
179
+ _context2.n = 2;
180
+ return screen.findByText('WooCommerce');
181
+ case 2:
182
+ _t(_context2.v).toBeInTheDocument();
183
+ _context2.n = 3;
184
+ return clickActivate();
185
+ case 3:
186
+ _t2 = expect;
187
+ _context2.n = 4;
188
+ return screen.findByText('Settings form');
189
+ case 4:
190
+ _t2(_context2.v).toBeInTheDocument();
191
+ _context2.n = 5;
192
+ return flushPending();
193
+ case 5:
194
+ expect(counts.wizard).toBe(2);
195
+ case 6:
196
+ return _context2.a(2);
197
+ }
198
+ }, _callee2);
199
+ })));
200
+ it('does not refetch when the required plugins were already active', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee3() {
201
+ var slug, counts, _t3;
202
+ return _regenerator().w(function (_context3) {
203
+ while (1) switch (_context3.n) {
204
+ case 0:
205
+ slug = 'test-wizard-no-refetch';
206
+ counts = mockEndpoints(slug, 'active');
207
+ renderWizard(slug);
208
+ _t3 = expect;
209
+ _context3.n = 1;
210
+ return screen.findByText('Settings form');
211
+ case 1:
212
+ _t3(_context3.v).toBeInTheDocument();
213
+ _context3.n = 2;
214
+ return flushPending();
215
+ case 2:
216
+ expect(counts.wizard).toBe(1);
217
+ case 3:
218
+ return _context3.a(2);
219
+ }
220
+ }, _callee3);
221
+ })));
222
+ it('fetches nothing for a wizard that has required plugins but no API slug', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee4() {
223
+ var slug, counts, _t4;
224
+ return _regenerator().w(function (_context4) {
225
+ while (1) switch (_context4.n) {
226
+ case 0:
227
+ // The shape of every other Newspack wizard that gates on plugins: it
228
+ // reads page-load globals rather than wizard API data. This change has to
229
+ // stay inert for them. Note this pins the inertness, not the `apiSlug`
230
+ // guard itself — the resolver already no-ops on an undefined slug, so
231
+ // that guard is a second line of defence rather than the load-bearing one.
232
+ slug = 'test-wizard-no-api-slug';
233
+ counts = mockEndpoints(slug);
234
+ renderWizard(slug, {
235
+ withApiSlug: false
236
+ });
237
+ _context4.n = 1;
238
+ return clickActivate();
239
+ case 1:
240
+ _t4 = expect;
241
+ _context4.n = 2;
242
+ return screen.findByText('Static section');
243
+ case 2:
244
+ _t4(_context4.v).toBeInTheDocument();
245
+ _context4.n = 3;
246
+ return flushPending();
247
+ case 3:
248
+ expect(counts.wizard).toBe(0);
249
+ case 4:
250
+ return _context4.a(2);
251
+ }
252
+ }, _callee4);
253
+ })));
254
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newspack-components",
3
- "version": "4.7.0-alpha.1",
3
+ "version": "4.7.0-alpha.2",
4
4
  "description": "Newspack design system components",
5
5
  "author": "Automattic",
6
6
  "license": "GPL-2.0-or-later",
@@ -9,7 +9,7 @@ import classnames from 'classnames';
9
9
  // Notice is aliased: `Notice` below is Newspack's own, which this file also uses.
10
10
  import { DropdownMenu, MenuGroup, MenuItem, Notice as CoreNotice, SlotFillProvider, createSlotFill } from '@wordpress/components';
11
11
  import { useDispatch, useSelect } from '@wordpress/data';
12
- import { cloneElement, createInterpolateElement, isValidElement, useEffect, useState, forwardRef } from '@wordpress/element';
12
+ import { cloneElement, createInterpolateElement, isValidElement, useRef, useEffect, useState, forwardRef } from '@wordpress/element';
13
13
  import { __ } from '@wordpress/i18n';
14
14
  import { category, chevronLeft, moreVertical } from '@wordpress/icons';
15
15
 
@@ -163,6 +163,7 @@ const Wizard = (
163
163
  const isQuietLoading = useSelect( select => select( WIZARD_STORE_NAMESPACE ).isQuietLoading() );
164
164
  const headerData = useSelect( select => select( WIZARD_STORE_NAMESPACE ).getHeaderData() );
165
165
  const notices = useSelect( select => select( WIZARD_STORE_NAMESPACE ).getNotices() );
166
+ const { invalidateResolution } = useDispatch( WIZARD_STORE_NAMESPACE );
166
167
  const { actions, backNav, badges, sectionDescription, sectionMenu, sectionName, sectionTitle, sectionPrimaryAction, sectionSecondaryAction } =
167
168
  headerData;
168
169
 
@@ -176,14 +177,36 @@ const Wizard = (
176
177
  let displayedSections = sections.filter( section => ! section.isHidden );
177
178
 
178
179
  const [ pluginRequirementsSatisfied, setPluginRequirementsSatisfied ] = useState( requiredPlugins.length === 0 );
180
+
181
+ // The data fetch above runs once per mount, while the required plugins are
182
+ // still missing — endpoints that need them answer empty or error outright,
183
+ // and either way the resolver records that as the answer. So the installer
184
+ // has to trigger a refetch, or the section mounts against it and renders
185
+ // nothing until the user saves. Requirements already met on mount are left
186
+ // alone: that fetch saw the real site, and refetching would cost every such
187
+ // wizard a second request on every load.
188
+ const requirementsWereUnmet = useRef( false );
189
+ const onPluginStatus = ( { complete } ) => {
190
+ // Leave the installer first: whatever happens to the refetch, the user
191
+ // should not be stranded on a required-plugins screen for a plugin they
192
+ // just installed.
193
+ setPluginRequirementsSatisfied( complete );
194
+ if ( ! complete ) {
195
+ requirementsWereUnmet.current = true;
196
+ } else if ( requirementsWereUnmet.current ) {
197
+ requirementsWereUnmet.current = false;
198
+ if ( apiSlug && isInitialFetchTriggered ) {
199
+ invalidateResolution( 'getWizardAPIData', [ apiSlug ] );
200
+ }
201
+ }
202
+ };
203
+
179
204
  if ( ! pluginRequirementsSatisfied ) {
180
205
  headerText = requiredPlugins.length > 1 ? __( 'Required plugins', 'newspack-plugin' ) : __( 'Required plugin', 'newspack-plugin' );
181
206
  displayedSections = [
182
207
  {
183
208
  path: '/',
184
- render: () => (
185
- <PluginInstaller plugins={ requiredPlugins } onStatus={ ( { complete } ) => setPluginRequirementsSatisfied( complete ) } />
186
- ),
209
+ render: () => <PluginInstaller plugins={ requiredPlugins } onStatus={ onPluginStatus } />,
187
210
  },
188
211
  ];
189
212
  }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * External dependencies.
3
+ */
4
+ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
5
+
6
+ /**
7
+ * WordPress dependencies.
8
+ */
9
+ import apiFetch from '@wordpress/api-fetch';
10
+
11
+ /**
12
+ * Internal dependencies.
13
+ */
14
+ import Wizard from './';
15
+ import { useWizardData } from './store/utils';
16
+
17
+ jest.mock( '@wordpress/api-fetch', () => jest.fn() );
18
+
19
+ // Both globals are localized onto every real wizard screen. The footer's
20
+ // ExternalLink needs a string href, and the debug Notice reads aux data.
21
+ window.newspack_aux_data = { is_debug_mode: false };
22
+ window.newspack_urls = { support: 'https://help.newspack.com/' };
23
+
24
+ const SETTINGS = { minimumDonation: '5' };
25
+
26
+ // Stands in for a wizard section that can only render once its API data has
27
+ // arrived — the shape a wizard section has when the wizard owns an apiSlug.
28
+ const Section = ( { slug } ) => {
29
+ const wizardData = useWizardData( slug );
30
+ return <div>{ wizardData.settings ? 'Settings form' : null }</div>;
31
+ };
32
+
33
+ // The other shape: a section on a wizard with no apiSlug, which reads page-load
34
+ // globals rather than wizard API data and so never touches the store.
35
+ const StaticSection = () => <div>Static section</div>;
36
+
37
+ // Mocks the endpoints a wizard hits, gated on whether the required plugin is
38
+ // active: the wizard's own endpoint 400s while the plugin is missing, exactly as
39
+ // Audience_Donations::api_get_donation_settings() does without WooCommerce.
40
+ // Counts every wizard-endpoint GET, whatever the slug, so a test can assert that
41
+ // a wizard given no apiSlug fetches nothing at all.
42
+ const mockEndpoints = ( slug, initialStatus = 'inactive' ) => {
43
+ const counts = { wizard: 0 };
44
+ let pluginStatus = initialStatus;
45
+ const plugin = () => ( { Name: 'WooCommerce', Description: 'Store', Status: pluginStatus, Download: 'wporg' } );
46
+ apiFetch.mockImplementation( ( { path, method } ) => {
47
+ if ( path.startsWith( '/newspack/v1/wizard/' ) && 'POST' !== method ) {
48
+ counts.wizard++;
49
+ return 'active' === pluginStatus
50
+ ? Promise.resolve( { settings: SETTINGS } )
51
+ : Promise.reject( {
52
+ code: 'newspack_missing_required_plugin',
53
+ message: 'The WooCommerce plugin is not installed and activated.',
54
+ } );
55
+ }
56
+ if ( path === '/newspack/v1/plugins/' ) {
57
+ return Promise.resolve( { woocommerce: plugin() } );
58
+ }
59
+ if ( path === '/newspack/v1/plugins/woocommerce/configure/' ) {
60
+ pluginStatus = 'active';
61
+ return Promise.resolve( plugin() );
62
+ }
63
+ return Promise.resolve( {} );
64
+ } );
65
+ return counts;
66
+ };
67
+
68
+ const renderWizard = ( slug, { withApiSlug = true } = {} ) =>
69
+ render(
70
+ <Wizard
71
+ headerText="Test wizard"
72
+ apiSlug={ withApiSlug ? slug : undefined }
73
+ requiredPlugins={ [ 'woocommerce' ] }
74
+ sections={ [
75
+ {
76
+ label: 'Configuration',
77
+ path: '/configuration',
78
+ render: () => ( withApiSlug ? <Section slug={ slug } /> : <StaticSection /> ),
79
+ },
80
+ ] }
81
+ />
82
+ );
83
+
84
+ // The plugin's own row button, not the footer's activate-all — both read
85
+ // "Activate", and the row button comes first in the DOM.
86
+ const clickActivate = async () => {
87
+ const [ activate ] = await screen.findAllByRole( 'button', { name: 'Activate' } );
88
+ fireEvent.click( activate );
89
+ };
90
+
91
+ // Lets every already-queued promise settle, so an assertion that something did
92
+ // NOT happen has actually given it the chance to.
93
+ const flushPending = () => act( () => new Promise( resolve => setTimeout( resolve, 0 ) ) );
94
+
95
+ describe( 'Wizard', () => {
96
+ beforeEach( () => {
97
+ apiFetch.mockReset();
98
+ } );
99
+
100
+ it( 'refetches its API data after the installer satisfies the plugin requirements', async () => {
101
+ // A distinct slug per test: the store's resolution cache lives in the
102
+ // module registry, so it is shared by every test in this file.
103
+ const slug = 'test-wizard-refetch';
104
+ const counts = mockEndpoints( slug );
105
+
106
+ renderWizard( slug );
107
+
108
+ // The first fetch runs before the plugin exists, so it fails. Until the
109
+ // installer is satisfied it owns the whole route, so the installer screen
110
+ // showing is what proves we are in the pre-activation state.
111
+ await waitFor( () => expect( counts.wizard ).toBe( 1 ) );
112
+ expect( await screen.findByText( 'WooCommerce' ) ).toBeInTheDocument();
113
+
114
+ await clickActivate();
115
+
116
+ // Activation makes the endpoint answerable, so the section must render
117
+ // against fresh data rather than the empty pre-activation response.
118
+ expect( await screen.findByText( 'Settings form' ) ).toBeInTheDocument();
119
+
120
+ // Exactly one refetch. The installer re-reports its status on every
121
+ // re-render while it is mounted, so this is the assertion that would
122
+ // catch a latch that re-fired.
123
+ await flushPending();
124
+ expect( counts.wizard ).toBe( 2 );
125
+ } );
126
+
127
+ it( 'does not refetch when the required plugins were already active', async () => {
128
+ const slug = 'test-wizard-no-refetch';
129
+ const counts = mockEndpoints( slug, 'active' );
130
+
131
+ renderWizard( slug );
132
+
133
+ expect( await screen.findByText( 'Settings form' ) ).toBeInTheDocument();
134
+ await flushPending();
135
+ expect( counts.wizard ).toBe( 1 );
136
+ } );
137
+
138
+ it( 'fetches nothing for a wizard that has required plugins but no API slug', async () => {
139
+ // The shape of every other Newspack wizard that gates on plugins: it
140
+ // reads page-load globals rather than wizard API data. This change has to
141
+ // stay inert for them. Note this pins the inertness, not the `apiSlug`
142
+ // guard itself — the resolver already no-ops on an undefined slug, so
143
+ // that guard is a second line of defence rather than the load-bearing one.
144
+ const slug = 'test-wizard-no-api-slug';
145
+ const counts = mockEndpoints( slug );
146
+
147
+ renderWizard( slug, { withApiSlug: false } );
148
+
149
+ await clickActivate();
150
+
151
+ // The installer still completes and hands the route back to the section.
152
+ expect( await screen.findByText( 'Static section' ) ).toBeInTheDocument();
153
+ await flushPending();
154
+ expect( counts.wizard ).toBe( 0 );
155
+ } );
156
+ } );