newspack-components 4.6.0 → 4.6.1-hotfix-wizard-stale-data-after-plugin-install.1
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 +27 -0
- package/dist/cjs/section-header/index.js +8 -8
- package/dist/cjs/wizard/index.js +27 -4
- package/dist/cjs/wizard/index.test.js +182 -0
- package/dist/esm/section-header/index.js +8 -8
- package/dist/esm/wizard/index.js +28 -5
- package/dist/esm/wizard/index.test.js +184 -0
- package/package.json +3 -3
- package/src/section-header/index.js +8 -7
- package/src/wizard/index.js +27 -4
- package/src/wizard/index.test.js +114 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## newspack-components [4.6.1-hotfix-wizard-stale-data-after-plugin-install.1](https://github.com/Automattic/newspack-workspace/compare/newspack-components@4.6.0...newspack-components@4.6.1-hotfix-wizard-stale-data-after-plugin-install.1) (2026-08-17)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **content-gate:** surface the Institutions entry point when institutions exist (NPPD-1492, [#708](https://github.com/Automattic/newspack-workspace/issues/708)) ([6ebb8c4](https://github.com/Automattic/newspack-workspace/commit/6ebb8c40f43b3f6d890900b683514eb9e6d65f2f))
|
|
7
|
+
* **wizard:** refetch data after the installer adds required plugins ([fd41402](https://github.com/Automattic/newspack-workspace/commit/fd414025a01e45faef49636a61c0bef247d72b2d))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Dependencies
|
|
11
|
+
|
|
12
|
+
* **newspack-icons:** upgraded to 1.1.1-hotfix-wizard-stale-data-after-plugin-install.1
|
|
13
|
+
* **newspack-scripts:** upgraded to 5.11.0-hotfix-wizard-stale-data-after-plugin-install.1
|
|
14
|
+
|
|
15
|
+
## newspack-components [4.6.1](https://github.com/Automattic/newspack-workspace/compare/newspack-components@4.6.0...newspack-components@4.6.1) (2026-08-17)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **content-gate:** surface the Institutions entry point when institutions exist (NPPD-1492, [#708](https://github.com/Automattic/newspack-workspace/issues/708)) ([6ebb8c4](https://github.com/Automattic/newspack-workspace/commit/6ebb8c40f43b3f6d890900b683514eb9e6d65f2f))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Dependencies
|
|
24
|
+
|
|
25
|
+
* **newspack-icons:** upgraded to 1.1.1
|
|
26
|
+
* **newspack-scripts:** upgraded to 5.11.0
|
|
27
|
+
|
|
1
28
|
# newspack-components [4.6.0](https://github.com/Automattic/newspack-workspace/compare/newspack-components@4.5.0...newspack-components@4.6.0) (2026-08-03)
|
|
2
29
|
|
|
3
30
|
|
|
@@ -120,6 +120,14 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
120
120
|
level: badge.level || 'default'
|
|
121
121
|
}, i);
|
|
122
122
|
}) : null]
|
|
123
|
+
}), secondaryAction && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
124
|
+
className: "newspack-section-header__secondary-action",
|
|
125
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_.Button, {
|
|
126
|
+
variant: "link",
|
|
127
|
+
href: secondaryAction.href,
|
|
128
|
+
onClick: secondaryAction.action,
|
|
129
|
+
children: secondaryAction.label
|
|
130
|
+
})
|
|
123
131
|
}), (menu === null || menu === void 0 ? void 0 : menu.length) > 0 && /*#__PURE__*/(0, _jsxRuntime.jsx)(_components.DropdownMenu, {
|
|
124
132
|
className: "newspack-section-header__menu",
|
|
125
133
|
icon: _icons.moreVertical,
|
|
@@ -136,14 +144,6 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
136
144
|
}, index);
|
|
137
145
|
});
|
|
138
146
|
}
|
|
139
|
-
}), secondaryAction && /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
140
|
-
className: "newspack-section-header__secondary-action",
|
|
141
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_.Button, {
|
|
142
|
-
variant: "link",
|
|
143
|
-
href: secondaryAction.href,
|
|
144
|
-
onClick: secondaryAction.action,
|
|
145
|
-
children: secondaryAction.label
|
|
146
|
-
})
|
|
147
147
|
})]
|
|
148
148
|
});
|
|
149
149
|
} else if (typeof title === 'function') {
|
package/dist/cjs/wizard/index.js
CHANGED
|
@@ -160,6 +160,8 @@ var Wizard = function Wizard(_ref2, ref) {
|
|
|
160
160
|
var notices = (0, _data.useSelect)(function (select) {
|
|
161
161
|
return select(_store.WIZARD_STORE_NAMESPACE).getNotices();
|
|
162
162
|
});
|
|
163
|
+
var _useDispatch2 = (0, _data.useDispatch)(_store.WIZARD_STORE_NAMESPACE),
|
|
164
|
+
invalidateResolution = _useDispatch2.invalidateResolution;
|
|
163
165
|
var actions = headerData.actions,
|
|
164
166
|
backNav = headerData.backNav,
|
|
165
167
|
badges = headerData.badges,
|
|
@@ -188,6 +190,30 @@ var Wizard = function Wizard(_ref2, ref) {
|
|
|
188
190
|
_useState2 = (0, _slicedToArray2["default"])(_useState, 2),
|
|
189
191
|
pluginRequirementsSatisfied = _useState2[0],
|
|
190
192
|
setPluginRequirementsSatisfied = _useState2[1];
|
|
193
|
+
|
|
194
|
+
// The data fetch above runs once per mount, while the required plugins are
|
|
195
|
+
// still missing — endpoints that need them answer empty or error outright,
|
|
196
|
+
// and either way the resolver records that as the answer. So the installer
|
|
197
|
+
// has to trigger a refetch, or the section mounts against it and renders
|
|
198
|
+
// nothing until the user saves. Requirements already met on mount are left
|
|
199
|
+
// alone: that fetch saw the real site, and refetching would cost every such
|
|
200
|
+
// wizard a second request on every load.
|
|
201
|
+
var requirementsWereUnmet = (0, _element.useRef)(false);
|
|
202
|
+
var onPluginStatus = function onPluginStatus(_ref3) {
|
|
203
|
+
var complete = _ref3.complete;
|
|
204
|
+
// Leave the installer first: whatever happens to the refetch, the user
|
|
205
|
+
// should not be stranded on a required-plugins screen for a plugin they
|
|
206
|
+
// just installed.
|
|
207
|
+
setPluginRequirementsSatisfied(complete);
|
|
208
|
+
if (!complete) {
|
|
209
|
+
requirementsWereUnmet.current = true;
|
|
210
|
+
} else if (requirementsWereUnmet.current) {
|
|
211
|
+
requirementsWereUnmet.current = false;
|
|
212
|
+
if (apiSlug && isInitialFetchTriggered) {
|
|
213
|
+
invalidateResolution('getWizardAPIData', [apiSlug]);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
191
217
|
if (!pluginRequirementsSatisfied) {
|
|
192
218
|
headerText = requiredPlugins.length > 1 ? (0, _i18n.__)('Required plugins', 'newspack-plugin') : (0, _i18n.__)('Required plugin', 'newspack-plugin');
|
|
193
219
|
displayedSections = [{
|
|
@@ -195,10 +221,7 @@ var Wizard = function Wizard(_ref2, ref) {
|
|
|
195
221
|
render: function render() {
|
|
196
222
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_.PluginInstaller, {
|
|
197
223
|
plugins: requiredPlugins,
|
|
198
|
-
onStatus:
|
|
199
|
-
var complete = _ref3.complete;
|
|
200
|
-
return setPluginRequirementsSatisfied(complete);
|
|
201
|
-
}
|
|
224
|
+
onStatus: onPluginStatus
|
|
202
225
|
});
|
|
203
226
|
}
|
|
204
227
|
}];
|
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
global.newspack_aux_data = {
|
|
23
|
+
is_debug_mode: false
|
|
24
|
+
};
|
|
25
|
+
// The wizard footer links out to the docs; both globals are localized onto every
|
|
26
|
+
// real wizard screen, and the footer's ExternalLink needs a string href.
|
|
27
|
+
window.newspack_urls = {
|
|
28
|
+
support: 'https://help.newspack.com/'
|
|
29
|
+
};
|
|
30
|
+
var SETTINGS = {
|
|
31
|
+
minimumDonation: '5'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Stands in for a wizard section that can only render once its API data has
|
|
35
|
+
// arrived — the shape every Newspack wizard section has.
|
|
36
|
+
var Section = function Section(_ref) {
|
|
37
|
+
var slug = _ref.slug;
|
|
38
|
+
var wizardData = (0, _utils.useWizardData)(slug);
|
|
39
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
|
|
40
|
+
children: wizardData.settings ? 'Settings form' : null
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Mocks the endpoints a wizard hits, gated on whether the required plugin is
|
|
45
|
+
// active: the wizard's own endpoint 400s while the plugin is missing, exactly as
|
|
46
|
+
// Audience_Donations::api_get_donation_settings() does without WooCommerce.
|
|
47
|
+
// Returns a counter so a test can assert how many times the wizard refetched.
|
|
48
|
+
var mockEndpoints = function mockEndpoints(slug) {
|
|
49
|
+
var initialStatus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'inactive';
|
|
50
|
+
var counts = {
|
|
51
|
+
wizard: 0
|
|
52
|
+
};
|
|
53
|
+
var pluginStatus = initialStatus;
|
|
54
|
+
var plugin = function plugin() {
|
|
55
|
+
return {
|
|
56
|
+
Name: 'WooCommerce',
|
|
57
|
+
Description: 'Store',
|
|
58
|
+
Status: pluginStatus,
|
|
59
|
+
Download: 'wporg'
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
_apiFetch["default"].mockImplementation(function (_ref2) {
|
|
63
|
+
var path = _ref2.path,
|
|
64
|
+
method = _ref2.method;
|
|
65
|
+
if (path === "/newspack/v1/wizard/".concat(slug) && 'POST' !== method) {
|
|
66
|
+
counts.wizard++;
|
|
67
|
+
return 'active' === pluginStatus ? Promise.resolve({
|
|
68
|
+
settings: SETTINGS
|
|
69
|
+
}) : Promise.reject({
|
|
70
|
+
code: 'newspack_missing_required_plugin',
|
|
71
|
+
message: 'The WooCommerce plugin is not installed and activated.'
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (path === '/newspack/v1/plugins/') {
|
|
75
|
+
return Promise.resolve({
|
|
76
|
+
woocommerce: plugin()
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (path === '/newspack/v1/plugins/woocommerce/configure/') {
|
|
80
|
+
pluginStatus = 'active';
|
|
81
|
+
return Promise.resolve(plugin());
|
|
82
|
+
}
|
|
83
|
+
return Promise.resolve({});
|
|
84
|
+
});
|
|
85
|
+
return counts;
|
|
86
|
+
};
|
|
87
|
+
var renderWizard = function renderWizard(slug) {
|
|
88
|
+
return (0, _react.render)(/*#__PURE__*/(0, _jsxRuntime.jsx)(_["default"], {
|
|
89
|
+
headerText: "Test wizard",
|
|
90
|
+
apiSlug: slug,
|
|
91
|
+
requiredPlugins: ['woocommerce'],
|
|
92
|
+
sections: [{
|
|
93
|
+
label: 'Configuration',
|
|
94
|
+
path: '/configuration',
|
|
95
|
+
render: function render() {
|
|
96
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(Section, {
|
|
97
|
+
slug: slug
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}]
|
|
101
|
+
}));
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Lets every already-queued promise settle, so an assertion that something did
|
|
105
|
+
// NOT happen has actually given it the chance to.
|
|
106
|
+
var flushPending = function flushPending() {
|
|
107
|
+
return (0, _react.act)(function () {
|
|
108
|
+
return new Promise(function (resolve) {
|
|
109
|
+
return setTimeout(resolve, 0);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
describe('Wizard', function () {
|
|
114
|
+
beforeEach(function () {
|
|
115
|
+
_apiFetch["default"].mockReset();
|
|
116
|
+
});
|
|
117
|
+
it('refetches its API data after the installer satisfies the plugin requirements', /*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee() {
|
|
118
|
+
var slug, counts, _yield$screen$findAll, _yield$screen$findAll2, activate, _t;
|
|
119
|
+
return _regenerator().w(function (_context) {
|
|
120
|
+
while (1) switch (_context.n) {
|
|
121
|
+
case 0:
|
|
122
|
+
// A distinct slug per test: the store's resolver cache is process-wide.
|
|
123
|
+
slug = 'test-wizard-refetch';
|
|
124
|
+
counts = mockEndpoints(slug);
|
|
125
|
+
renderWizard(slug);
|
|
126
|
+
|
|
127
|
+
// The first fetch runs before the plugin exists, so it fails.
|
|
128
|
+
_context.n = 1;
|
|
129
|
+
return (0, _react.waitFor)(function () {
|
|
130
|
+
return expect(counts.wizard).toBe(1);
|
|
131
|
+
});
|
|
132
|
+
case 1:
|
|
133
|
+
expect(_react.screen.queryByText('Settings form')).not.toBeInTheDocument();
|
|
134
|
+
|
|
135
|
+
// Two buttons read "Activate": the plugin's own row and the footer's
|
|
136
|
+
// activate-all. The row button comes first in the DOM.
|
|
137
|
+
_context.n = 2;
|
|
138
|
+
return _react.screen.findAllByRole('button', {
|
|
139
|
+
name: 'Activate'
|
|
140
|
+
});
|
|
141
|
+
case 2:
|
|
142
|
+
_yield$screen$findAll = _context.v;
|
|
143
|
+
_yield$screen$findAll2 = (0, _slicedToArray2["default"])(_yield$screen$findAll, 1);
|
|
144
|
+
activate = _yield$screen$findAll2[0];
|
|
145
|
+
_react.fireEvent.click(activate);
|
|
146
|
+
|
|
147
|
+
// Activation makes the endpoint answerable, so the section must render
|
|
148
|
+
// against fresh data rather than the empty pre-activation response.
|
|
149
|
+
_t = expect;
|
|
150
|
+
_context.n = 3;
|
|
151
|
+
return _react.screen.findByText('Settings form');
|
|
152
|
+
case 3:
|
|
153
|
+
_t(_context.v).toBeInTheDocument();
|
|
154
|
+
expect(counts.wizard).toBe(2);
|
|
155
|
+
case 4:
|
|
156
|
+
return _context.a(2);
|
|
157
|
+
}
|
|
158
|
+
}, _callee);
|
|
159
|
+
})));
|
|
160
|
+
it('does not refetch when the required plugins were already active', /*#__PURE__*/(0, _asyncToGenerator2["default"])(/*#__PURE__*/_regenerator().m(function _callee2() {
|
|
161
|
+
var slug, counts, _t2;
|
|
162
|
+
return _regenerator().w(function (_context2) {
|
|
163
|
+
while (1) switch (_context2.n) {
|
|
164
|
+
case 0:
|
|
165
|
+
slug = 'test-wizard-no-refetch';
|
|
166
|
+
counts = mockEndpoints(slug, 'active');
|
|
167
|
+
renderWizard(slug);
|
|
168
|
+
_t2 = expect;
|
|
169
|
+
_context2.n = 1;
|
|
170
|
+
return _react.screen.findByText('Settings form');
|
|
171
|
+
case 1:
|
|
172
|
+
_t2(_context2.v).toBeInTheDocument();
|
|
173
|
+
_context2.n = 2;
|
|
174
|
+
return flushPending();
|
|
175
|
+
case 2:
|
|
176
|
+
expect(counts.wizard).toBe(1);
|
|
177
|
+
case 3:
|
|
178
|
+
return _context2.a(2);
|
|
179
|
+
}
|
|
180
|
+
}, _callee2);
|
|
181
|
+
})));
|
|
182
|
+
});
|
|
@@ -112,6 +112,14 @@ var SectionHeader = function SectionHeader(_ref) {
|
|
|
112
112
|
level: badge.level || 'default'
|
|
113
113
|
}, i);
|
|
114
114
|
}) : null]
|
|
115
|
+
}), secondaryAction && /*#__PURE__*/_jsx("div", {
|
|
116
|
+
className: "newspack-section-header__secondary-action",
|
|
117
|
+
children: /*#__PURE__*/_jsx(Button, {
|
|
118
|
+
variant: "link",
|
|
119
|
+
href: secondaryAction.href,
|
|
120
|
+
onClick: secondaryAction.action,
|
|
121
|
+
children: secondaryAction.label
|
|
122
|
+
})
|
|
115
123
|
}), (menu === null || menu === void 0 ? void 0 : menu.length) > 0 && /*#__PURE__*/_jsx(DropdownMenu, {
|
|
116
124
|
className: "newspack-section-header__menu",
|
|
117
125
|
icon: moreVertical,
|
|
@@ -128,14 +136,6 @@ var SectionHeader = function SectionHeader(_ref) {
|
|
|
128
136
|
}, index);
|
|
129
137
|
});
|
|
130
138
|
}
|
|
131
|
-
}), secondaryAction && /*#__PURE__*/_jsx("div", {
|
|
132
|
-
className: "newspack-section-header__secondary-action",
|
|
133
|
-
children: /*#__PURE__*/_jsx(Button, {
|
|
134
|
-
variant: "link",
|
|
135
|
-
href: secondaryAction.href,
|
|
136
|
-
onClick: secondaryAction.action,
|
|
137
|
-
children: secondaryAction.label
|
|
138
|
-
})
|
|
139
139
|
})]
|
|
140
140
|
});
|
|
141
141
|
} else if (typeof title === 'function') {
|
package/dist/esm/wizard/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import classnames from 'classnames';
|
|
|
12
12
|
*/
|
|
13
13
|
import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components';
|
|
14
14
|
import { useDispatch, useSelect } from '@wordpress/data';
|
|
15
|
-
import { cloneElement, isValidElement, useEffect, useState, forwardRef } from '@wordpress/element';
|
|
15
|
+
import { cloneElement, isValidElement, useEffect, useRef, useState, forwardRef } from '@wordpress/element';
|
|
16
16
|
import { __ } from '@wordpress/i18n';
|
|
17
17
|
import { category, chevronLeft, moreVertical } from '@wordpress/icons';
|
|
18
18
|
|
|
@@ -157,6 +157,8 @@ var Wizard = function Wizard(_ref2, ref) {
|
|
|
157
157
|
var notices = useSelect(function (select) {
|
|
158
158
|
return select(WIZARD_STORE_NAMESPACE).getNotices();
|
|
159
159
|
});
|
|
160
|
+
var _useDispatch2 = useDispatch(WIZARD_STORE_NAMESPACE),
|
|
161
|
+
invalidateResolution = _useDispatch2.invalidateResolution;
|
|
160
162
|
var actions = headerData.actions,
|
|
161
163
|
backNav = headerData.backNav,
|
|
162
164
|
badges = headerData.badges,
|
|
@@ -185,6 +187,30 @@ var Wizard = function Wizard(_ref2, ref) {
|
|
|
185
187
|
_useState2 = _slicedToArray(_useState, 2),
|
|
186
188
|
pluginRequirementsSatisfied = _useState2[0],
|
|
187
189
|
setPluginRequirementsSatisfied = _useState2[1];
|
|
190
|
+
|
|
191
|
+
// The data fetch above runs once per mount, while the required plugins are
|
|
192
|
+
// still missing — endpoints that need them answer empty or error outright,
|
|
193
|
+
// and either way the resolver records that as the answer. So the installer
|
|
194
|
+
// has to trigger a refetch, or the section mounts against it and renders
|
|
195
|
+
// nothing until the user saves. Requirements already met on mount are left
|
|
196
|
+
// alone: that fetch saw the real site, and refetching would cost every such
|
|
197
|
+
// wizard a second request on every load.
|
|
198
|
+
var requirementsWereUnmet = useRef(false);
|
|
199
|
+
var onPluginStatus = function onPluginStatus(_ref3) {
|
|
200
|
+
var complete = _ref3.complete;
|
|
201
|
+
// Leave the installer first: whatever happens to the refetch, the user
|
|
202
|
+
// should not be stranded on a required-plugins screen for a plugin they
|
|
203
|
+
// just installed.
|
|
204
|
+
setPluginRequirementsSatisfied(complete);
|
|
205
|
+
if (!complete) {
|
|
206
|
+
requirementsWereUnmet.current = true;
|
|
207
|
+
} else if (requirementsWereUnmet.current) {
|
|
208
|
+
requirementsWereUnmet.current = false;
|
|
209
|
+
if (apiSlug && isInitialFetchTriggered) {
|
|
210
|
+
invalidateResolution('getWizardAPIData', [apiSlug]);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
188
214
|
if (!pluginRequirementsSatisfied) {
|
|
189
215
|
headerText = requiredPlugins.length > 1 ? __('Required plugins', 'newspack-plugin') : __('Required plugin', 'newspack-plugin');
|
|
190
216
|
displayedSections = [{
|
|
@@ -192,10 +218,7 @@ var Wizard = function Wizard(_ref2, ref) {
|
|
|
192
218
|
render: function render() {
|
|
193
219
|
return /*#__PURE__*/_jsx(PluginInstaller, {
|
|
194
220
|
plugins: requiredPlugins,
|
|
195
|
-
onStatus:
|
|
196
|
-
var complete = _ref3.complete;
|
|
197
|
-
return setPluginRequirementsSatisfied(complete);
|
|
198
|
-
}
|
|
221
|
+
onStatus: onPluginStatus
|
|
199
222
|
});
|
|
200
223
|
}
|
|
201
224
|
}];
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
global.newspack_aux_data = {
|
|
25
|
+
is_debug_mode: false
|
|
26
|
+
};
|
|
27
|
+
// The wizard footer links out to the docs; both globals are localized onto every
|
|
28
|
+
// real wizard screen, and the footer's ExternalLink needs a string href.
|
|
29
|
+
window.newspack_urls = {
|
|
30
|
+
support: 'https://help.newspack.com/'
|
|
31
|
+
};
|
|
32
|
+
var SETTINGS = {
|
|
33
|
+
minimumDonation: '5'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Stands in for a wizard section that can only render once its API data has
|
|
37
|
+
// arrived — the shape every Newspack wizard section has.
|
|
38
|
+
var Section = function Section(_ref) {
|
|
39
|
+
var slug = _ref.slug;
|
|
40
|
+
var wizardData = useWizardData(slug);
|
|
41
|
+
return /*#__PURE__*/_jsx("div", {
|
|
42
|
+
children: wizardData.settings ? 'Settings form' : null
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Mocks the endpoints a wizard hits, gated on whether the required plugin is
|
|
47
|
+
// active: the wizard's own endpoint 400s while the plugin is missing, exactly as
|
|
48
|
+
// Audience_Donations::api_get_donation_settings() does without WooCommerce.
|
|
49
|
+
// Returns a counter so a test can assert how many times the wizard refetched.
|
|
50
|
+
var mockEndpoints = function mockEndpoints(slug) {
|
|
51
|
+
var initialStatus = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'inactive';
|
|
52
|
+
var counts = {
|
|
53
|
+
wizard: 0
|
|
54
|
+
};
|
|
55
|
+
var pluginStatus = initialStatus;
|
|
56
|
+
var plugin = function plugin() {
|
|
57
|
+
return {
|
|
58
|
+
Name: 'WooCommerce',
|
|
59
|
+
Description: 'Store',
|
|
60
|
+
Status: pluginStatus,
|
|
61
|
+
Download: 'wporg'
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
apiFetch.mockImplementation(function (_ref2) {
|
|
65
|
+
var path = _ref2.path,
|
|
66
|
+
method = _ref2.method;
|
|
67
|
+
if (path === "/newspack/v1/wizard/".concat(slug) && 'POST' !== method) {
|
|
68
|
+
counts.wizard++;
|
|
69
|
+
return 'active' === pluginStatus ? Promise.resolve({
|
|
70
|
+
settings: SETTINGS
|
|
71
|
+
}) : Promise.reject({
|
|
72
|
+
code: 'newspack_missing_required_plugin',
|
|
73
|
+
message: 'The WooCommerce plugin is not installed and activated.'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (path === '/newspack/v1/plugins/') {
|
|
77
|
+
return Promise.resolve({
|
|
78
|
+
woocommerce: plugin()
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (path === '/newspack/v1/plugins/woocommerce/configure/') {
|
|
82
|
+
pluginStatus = 'active';
|
|
83
|
+
return Promise.resolve(plugin());
|
|
84
|
+
}
|
|
85
|
+
return Promise.resolve({});
|
|
86
|
+
});
|
|
87
|
+
return counts;
|
|
88
|
+
};
|
|
89
|
+
var renderWizard = function renderWizard(slug) {
|
|
90
|
+
return render(/*#__PURE__*/_jsx(Wizard, {
|
|
91
|
+
headerText: "Test wizard",
|
|
92
|
+
apiSlug: slug,
|
|
93
|
+
requiredPlugins: ['woocommerce'],
|
|
94
|
+
sections: [{
|
|
95
|
+
label: 'Configuration',
|
|
96
|
+
path: '/configuration',
|
|
97
|
+
render: function render() {
|
|
98
|
+
return /*#__PURE__*/_jsx(Section, {
|
|
99
|
+
slug: slug
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}]
|
|
103
|
+
}));
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Lets every already-queued promise settle, so an assertion that something did
|
|
107
|
+
// NOT happen has actually given it the chance to.
|
|
108
|
+
var flushPending = function flushPending() {
|
|
109
|
+
return act(function () {
|
|
110
|
+
return new Promise(function (resolve) {
|
|
111
|
+
return setTimeout(resolve, 0);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
describe('Wizard', function () {
|
|
116
|
+
beforeEach(function () {
|
|
117
|
+
apiFetch.mockReset();
|
|
118
|
+
});
|
|
119
|
+
it('refetches its API data after the installer satisfies the plugin requirements', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
|
|
120
|
+
var slug, counts, _yield$screen$findAll, _yield$screen$findAll2, activate, _t;
|
|
121
|
+
return _regenerator().w(function (_context) {
|
|
122
|
+
while (1) switch (_context.n) {
|
|
123
|
+
case 0:
|
|
124
|
+
// A distinct slug per test: the store's resolver cache is process-wide.
|
|
125
|
+
slug = 'test-wizard-refetch';
|
|
126
|
+
counts = mockEndpoints(slug);
|
|
127
|
+
renderWizard(slug);
|
|
128
|
+
|
|
129
|
+
// The first fetch runs before the plugin exists, so it fails.
|
|
130
|
+
_context.n = 1;
|
|
131
|
+
return waitFor(function () {
|
|
132
|
+
return expect(counts.wizard).toBe(1);
|
|
133
|
+
});
|
|
134
|
+
case 1:
|
|
135
|
+
expect(screen.queryByText('Settings form')).not.toBeInTheDocument();
|
|
136
|
+
|
|
137
|
+
// Two buttons read "Activate": the plugin's own row and the footer's
|
|
138
|
+
// activate-all. The row button comes first in the DOM.
|
|
139
|
+
_context.n = 2;
|
|
140
|
+
return screen.findAllByRole('button', {
|
|
141
|
+
name: 'Activate'
|
|
142
|
+
});
|
|
143
|
+
case 2:
|
|
144
|
+
_yield$screen$findAll = _context.v;
|
|
145
|
+
_yield$screen$findAll2 = _slicedToArray(_yield$screen$findAll, 1);
|
|
146
|
+
activate = _yield$screen$findAll2[0];
|
|
147
|
+
fireEvent.click(activate);
|
|
148
|
+
|
|
149
|
+
// Activation makes the endpoint answerable, so the section must render
|
|
150
|
+
// against fresh data rather than the empty pre-activation response.
|
|
151
|
+
_t = expect;
|
|
152
|
+
_context.n = 3;
|
|
153
|
+
return screen.findByText('Settings form');
|
|
154
|
+
case 3:
|
|
155
|
+
_t(_context.v).toBeInTheDocument();
|
|
156
|
+
expect(counts.wizard).toBe(2);
|
|
157
|
+
case 4:
|
|
158
|
+
return _context.a(2);
|
|
159
|
+
}
|
|
160
|
+
}, _callee);
|
|
161
|
+
})));
|
|
162
|
+
it('does not refetch when the required plugins were already active', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
|
|
163
|
+
var slug, counts, _t2;
|
|
164
|
+
return _regenerator().w(function (_context2) {
|
|
165
|
+
while (1) switch (_context2.n) {
|
|
166
|
+
case 0:
|
|
167
|
+
slug = 'test-wizard-no-refetch';
|
|
168
|
+
counts = mockEndpoints(slug, 'active');
|
|
169
|
+
renderWizard(slug);
|
|
170
|
+
_t2 = expect;
|
|
171
|
+
_context2.n = 1;
|
|
172
|
+
return screen.findByText('Settings form');
|
|
173
|
+
case 1:
|
|
174
|
+
_t2(_context2.v).toBeInTheDocument();
|
|
175
|
+
_context2.n = 2;
|
|
176
|
+
return flushPending();
|
|
177
|
+
case 2:
|
|
178
|
+
expect(counts.wizard).toBe(1);
|
|
179
|
+
case 3:
|
|
180
|
+
return _context2.a(2);
|
|
181
|
+
}
|
|
182
|
+
}, _callee2);
|
|
183
|
+
})));
|
|
184
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newspack-components",
|
|
3
|
-
"version": "4.6.
|
|
3
|
+
"version": "4.6.1-hotfix-wizard-stale-data-after-plugin-install.1",
|
|
4
4
|
"description": "Newspack design system components",
|
|
5
5
|
"author": "Automattic",
|
|
6
6
|
"license": "GPL-2.0-or-later",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"classnames": "^2.3.1",
|
|
31
31
|
"lodash": "^4.17.21",
|
|
32
32
|
"moment-range": "^3.1.1",
|
|
33
|
-
"newspack-icons": "1.1.
|
|
33
|
+
"newspack-icons": "1.1.1-hotfix-wizard-stale-data-after-plugin-install.1",
|
|
34
34
|
"qs": "^6.15.2",
|
|
35
35
|
"react-router-dom": "^5.3.4",
|
|
36
36
|
"@babel/runtime": "^7.29.7",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@babel/preset-env": "^7.29.7",
|
|
59
59
|
"@babel/preset-react": "^7.29.7",
|
|
60
60
|
"@babel/preset-typescript": "^7.29.7",
|
|
61
|
-
"newspack-scripts": "5.
|
|
61
|
+
"newspack-scripts": "5.11.0-hotfix-wizard-stale-data-after-plugin-install.1",
|
|
62
62
|
"recursive-copy": "^2.0.0"
|
|
63
63
|
},
|
|
64
64
|
"babel": {
|
|
@@ -102,6 +102,14 @@ const SectionHeader = ( {
|
|
|
102
102
|
? badges.map( ( badge, i ) => <Badge key={ i } text={ badge.label } level={ badge.level || 'default' } /> )
|
|
103
103
|
: null }
|
|
104
104
|
</HeadingTag>
|
|
105
|
+
{ /* Secondary action before the overflow menu, so a promoted link reads as an action rather than sitting to the right of the kebab. */ }
|
|
106
|
+
{ secondaryAction && (
|
|
107
|
+
<div className="newspack-section-header__secondary-action">
|
|
108
|
+
<Button variant="link" href={ secondaryAction.href } onClick={ secondaryAction.action }>
|
|
109
|
+
{ secondaryAction.label }
|
|
110
|
+
</Button>
|
|
111
|
+
</div>
|
|
112
|
+
) }
|
|
105
113
|
{ menu?.length > 0 && (
|
|
106
114
|
<DropdownMenu className="newspack-section-header__menu" icon={ moreVertical } label={ __( 'More options', 'newspack-plugin' ) }>
|
|
107
115
|
{ () =>
|
|
@@ -120,13 +128,6 @@ const SectionHeader = ( {
|
|
|
120
128
|
}
|
|
121
129
|
</DropdownMenu>
|
|
122
130
|
) }
|
|
123
|
-
{ secondaryAction && (
|
|
124
|
-
<div className="newspack-section-header__secondary-action">
|
|
125
|
-
<Button variant="link" href={ secondaryAction.href } onClick={ secondaryAction.action }>
|
|
126
|
-
{ secondaryAction.label }
|
|
127
|
-
</Button>
|
|
128
|
-
</div>
|
|
129
|
-
) }
|
|
130
131
|
</div>
|
|
131
132
|
);
|
|
132
133
|
} else if ( typeof title === 'function' ) {
|
package/src/wizard/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import classnames from 'classnames';
|
|
|
8
8
|
*/
|
|
9
9
|
import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components';
|
|
10
10
|
import { useDispatch, useSelect } from '@wordpress/data';
|
|
11
|
-
import { cloneElement, isValidElement, useEffect, useState, forwardRef } from '@wordpress/element';
|
|
11
|
+
import { cloneElement, isValidElement, useEffect, useRef, useState, forwardRef } from '@wordpress/element';
|
|
12
12
|
import { __ } from '@wordpress/i18n';
|
|
13
13
|
import { category, chevronLeft, moreVertical } from '@wordpress/icons';
|
|
14
14
|
|
|
@@ -126,6 +126,7 @@ const Wizard = (
|
|
|
126
126
|
const isQuietLoading = useSelect( select => select( WIZARD_STORE_NAMESPACE ).isQuietLoading() );
|
|
127
127
|
const headerData = useSelect( select => select( WIZARD_STORE_NAMESPACE ).getHeaderData() );
|
|
128
128
|
const notices = useSelect( select => select( WIZARD_STORE_NAMESPACE ).getNotices() );
|
|
129
|
+
const { invalidateResolution } = useDispatch( WIZARD_STORE_NAMESPACE );
|
|
129
130
|
const { actions, backNav, badges, sectionDescription, sectionMenu, sectionName, sectionTitle, sectionPrimaryAction, sectionSecondaryAction } =
|
|
130
131
|
headerData;
|
|
131
132
|
|
|
@@ -139,14 +140,36 @@ const Wizard = (
|
|
|
139
140
|
let displayedSections = sections.filter( section => ! section.isHidden );
|
|
140
141
|
|
|
141
142
|
const [ pluginRequirementsSatisfied, setPluginRequirementsSatisfied ] = useState( requiredPlugins.length === 0 );
|
|
143
|
+
|
|
144
|
+
// The data fetch above runs once per mount, while the required plugins are
|
|
145
|
+
// still missing — endpoints that need them answer empty or error outright,
|
|
146
|
+
// and either way the resolver records that as the answer. So the installer
|
|
147
|
+
// has to trigger a refetch, or the section mounts against it and renders
|
|
148
|
+
// nothing until the user saves. Requirements already met on mount are left
|
|
149
|
+
// alone: that fetch saw the real site, and refetching would cost every such
|
|
150
|
+
// wizard a second request on every load.
|
|
151
|
+
const requirementsWereUnmet = useRef( false );
|
|
152
|
+
const onPluginStatus = ( { complete } ) => {
|
|
153
|
+
// Leave the installer first: whatever happens to the refetch, the user
|
|
154
|
+
// should not be stranded on a required-plugins screen for a plugin they
|
|
155
|
+
// just installed.
|
|
156
|
+
setPluginRequirementsSatisfied( complete );
|
|
157
|
+
if ( ! complete ) {
|
|
158
|
+
requirementsWereUnmet.current = true;
|
|
159
|
+
} else if ( requirementsWereUnmet.current ) {
|
|
160
|
+
requirementsWereUnmet.current = false;
|
|
161
|
+
if ( apiSlug && isInitialFetchTriggered ) {
|
|
162
|
+
invalidateResolution( 'getWizardAPIData', [ apiSlug ] );
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
142
167
|
if ( ! pluginRequirementsSatisfied ) {
|
|
143
168
|
headerText = requiredPlugins.length > 1 ? __( 'Required plugins', 'newspack-plugin' ) : __( 'Required plugin', 'newspack-plugin' );
|
|
144
169
|
displayedSections = [
|
|
145
170
|
{
|
|
146
171
|
path: '/',
|
|
147
|
-
render: () =>
|
|
148
|
-
<PluginInstaller plugins={ requiredPlugins } onStatus={ ( { complete } ) => setPluginRequirementsSatisfied( complete ) } />
|
|
149
|
-
),
|
|
172
|
+
render: () => <PluginInstaller plugins={ requiredPlugins } onStatus={ onPluginStatus } />,
|
|
150
173
|
},
|
|
151
174
|
];
|
|
152
175
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
global.newspack_aux_data = { is_debug_mode: false };
|
|
20
|
+
// The wizard footer links out to the docs; both globals are localized onto every
|
|
21
|
+
// real wizard screen, and the footer's ExternalLink needs a string href.
|
|
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 every Newspack wizard section has.
|
|
28
|
+
const Section = ( { slug } ) => {
|
|
29
|
+
const wizardData = useWizardData( slug );
|
|
30
|
+
return <div>{ wizardData.settings ? 'Settings form' : null }</div>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Mocks the endpoints a wizard hits, gated on whether the required plugin is
|
|
34
|
+
// active: the wizard's own endpoint 400s while the plugin is missing, exactly as
|
|
35
|
+
// Audience_Donations::api_get_donation_settings() does without WooCommerce.
|
|
36
|
+
// Returns a counter so a test can assert how many times the wizard refetched.
|
|
37
|
+
const mockEndpoints = ( slug, initialStatus = 'inactive' ) => {
|
|
38
|
+
const counts = { wizard: 0 };
|
|
39
|
+
let pluginStatus = initialStatus;
|
|
40
|
+
const plugin = () => ( { Name: 'WooCommerce', Description: 'Store', Status: pluginStatus, Download: 'wporg' } );
|
|
41
|
+
apiFetch.mockImplementation( ( { path, method } ) => {
|
|
42
|
+
if ( path === `/newspack/v1/wizard/${ slug }` && 'POST' !== method ) {
|
|
43
|
+
counts.wizard++;
|
|
44
|
+
return 'active' === pluginStatus
|
|
45
|
+
? Promise.resolve( { settings: SETTINGS } )
|
|
46
|
+
: Promise.reject( {
|
|
47
|
+
code: 'newspack_missing_required_plugin',
|
|
48
|
+
message: 'The WooCommerce plugin is not installed and activated.',
|
|
49
|
+
} );
|
|
50
|
+
}
|
|
51
|
+
if ( path === '/newspack/v1/plugins/' ) {
|
|
52
|
+
return Promise.resolve( { woocommerce: plugin() } );
|
|
53
|
+
}
|
|
54
|
+
if ( path === '/newspack/v1/plugins/woocommerce/configure/' ) {
|
|
55
|
+
pluginStatus = 'active';
|
|
56
|
+
return Promise.resolve( plugin() );
|
|
57
|
+
}
|
|
58
|
+
return Promise.resolve( {} );
|
|
59
|
+
} );
|
|
60
|
+
return counts;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const renderWizard = slug =>
|
|
64
|
+
render(
|
|
65
|
+
<Wizard
|
|
66
|
+
headerText="Test wizard"
|
|
67
|
+
apiSlug={ slug }
|
|
68
|
+
requiredPlugins={ [ 'woocommerce' ] }
|
|
69
|
+
sections={ [ { label: 'Configuration', path: '/configuration', render: () => <Section slug={ slug } /> } ] }
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Lets every already-queued promise settle, so an assertion that something did
|
|
74
|
+
// NOT happen has actually given it the chance to.
|
|
75
|
+
const flushPending = () => act( () => new Promise( resolve => setTimeout( resolve, 0 ) ) );
|
|
76
|
+
|
|
77
|
+
describe( 'Wizard', () => {
|
|
78
|
+
beforeEach( () => {
|
|
79
|
+
apiFetch.mockReset();
|
|
80
|
+
} );
|
|
81
|
+
|
|
82
|
+
it( 'refetches its API data after the installer satisfies the plugin requirements', async () => {
|
|
83
|
+
// A distinct slug per test: the store's resolver cache is process-wide.
|
|
84
|
+
const slug = 'test-wizard-refetch';
|
|
85
|
+
const counts = mockEndpoints( slug );
|
|
86
|
+
|
|
87
|
+
renderWizard( slug );
|
|
88
|
+
|
|
89
|
+
// The first fetch runs before the plugin exists, so it fails.
|
|
90
|
+
await waitFor( () => expect( counts.wizard ).toBe( 1 ) );
|
|
91
|
+
expect( screen.queryByText( 'Settings form' ) ).not.toBeInTheDocument();
|
|
92
|
+
|
|
93
|
+
// Two buttons read "Activate": the plugin's own row and the footer's
|
|
94
|
+
// activate-all. The row button comes first in the DOM.
|
|
95
|
+
const [ activate ] = await screen.findAllByRole( 'button', { name: 'Activate' } );
|
|
96
|
+
fireEvent.click( activate );
|
|
97
|
+
|
|
98
|
+
// Activation makes the endpoint answerable, so the section must render
|
|
99
|
+
// against fresh data rather than the empty pre-activation response.
|
|
100
|
+
expect( await screen.findByText( 'Settings form' ) ).toBeInTheDocument();
|
|
101
|
+
expect( counts.wizard ).toBe( 2 );
|
|
102
|
+
} );
|
|
103
|
+
|
|
104
|
+
it( 'does not refetch when the required plugins were already active', async () => {
|
|
105
|
+
const slug = 'test-wizard-no-refetch';
|
|
106
|
+
const counts = mockEndpoints( slug, 'active' );
|
|
107
|
+
|
|
108
|
+
renderWizard( slug );
|
|
109
|
+
|
|
110
|
+
expect( await screen.findByText( 'Settings form' ) ).toBeInTheDocument();
|
|
111
|
+
await flushPending();
|
|
112
|
+
expect( counts.wizard ).toBe( 1 );
|
|
113
|
+
} );
|
|
114
|
+
} );
|