catalyst-core 1.0.1-canary.1 → 1.0.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/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/SafeAreaUtils.kt +4 -2
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/utils/SafeAreaUtilsTest.kt +19 -2
- package/dist/otel.js +1 -34
- package/dist/server/renderer/handler.jsx +0 -23
- package/dist/vite/vite.config.client.js +1 -1
- package/dist/vite/vite.config.js +4 -0
- package/dist/web-router/components/MetaTag.jsx +13 -6
- package/dist/web-router/components/MetaTag.test.jsx +33 -40
- package/package.json +7 -2
- package/test/server/handler.test.ts +0 -27
|
@@ -31,12 +31,14 @@ object SafeAreaUtils {
|
|
|
31
31
|
* CSS px equal dp here: the WebView has no wide-viewport scaling, so
|
|
32
32
|
* devicePixelRatio == density and px / density gives CSS px.
|
|
33
33
|
* - Edge-to-edge disabled: Returns ZERO (the system already insets the WebView, iOS parity)
|
|
34
|
-
* - Edge-to-edge enabled: Returns ceil(max(system bars, cutout) / density) per edge
|
|
34
|
+
* - Edge-to-edge enabled: Returns ceil(max(system bars ignoring visibility, cutout) / density) per edge
|
|
35
35
|
*/
|
|
36
36
|
fun fromWindowInsets(insets: WindowInsetsCompat?, edgeToEdgeEnabled: Boolean, density: Float): SafeAreaInsets {
|
|
37
37
|
if (insets == null || !edgeToEdgeEnabled) return SafeAreaInsets.ZERO
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// Ignore visibility: the IME window hosts the nav bar, so the visible systemBars
|
|
40
|
+
// bottom grows to the keyboard height while the keyboard is open
|
|
41
|
+
val systemBars = insets.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.systemBars())
|
|
40
42
|
val cutoutInsets = insets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
|
41
43
|
|
|
42
44
|
return SafeAreaInsets(
|
|
@@ -32,9 +32,14 @@ import org.mockito.kotlin.whenever
|
|
|
32
32
|
*/
|
|
33
33
|
class SafeAreaUtilsTest {
|
|
34
34
|
|
|
35
|
-
private fun windowInsetsCompatWith(
|
|
35
|
+
private fun windowInsetsCompatWith(
|
|
36
|
+
systemBars: Insets,
|
|
37
|
+
cutout: Insets = Insets.NONE,
|
|
38
|
+
visibleSystemBars: Insets = systemBars
|
|
39
|
+
): WindowInsetsCompat {
|
|
36
40
|
return mock {
|
|
37
|
-
on { getInsets(WindowInsetsCompat.Type.systemBars()) } doReturn
|
|
41
|
+
on { getInsets(WindowInsetsCompat.Type.systemBars()) } doReturn visibleSystemBars
|
|
42
|
+
on { getInsetsIgnoringVisibility(WindowInsetsCompat.Type.systemBars()) } doReturn systemBars
|
|
38
43
|
on { getInsets(WindowInsetsCompat.Type.displayCutout()) } doReturn cutout
|
|
39
44
|
}
|
|
40
45
|
}
|
|
@@ -128,6 +133,18 @@ class SafeAreaUtilsTest {
|
|
|
128
133
|
assertEquals(SafeAreaInsets(top = 24, right = 0, bottom = 0, left = 0), result)
|
|
129
134
|
}
|
|
130
135
|
|
|
136
|
+
@Test
|
|
137
|
+
fun `fromWindowInsets with edge-to-edge enabled ignores the IME-inflated visible system bar bottom`() {
|
|
138
|
+
val windowInsets = windowInsetsCompatWith(
|
|
139
|
+
systemBars = Insets.of(0, 0, 0, 63),
|
|
140
|
+
visibleSystemBars = Insets.of(0, 0, 0, 883)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
val result = SafeAreaUtils.fromWindowInsets(windowInsets, edgeToEdgeEnabled = true, density = 2.625f)
|
|
144
|
+
|
|
145
|
+
assertEquals(SafeAreaInsets(top = 0, right = 0, bottom = 24, left = 0), result)
|
|
146
|
+
}
|
|
147
|
+
|
|
131
148
|
// ============================================================
|
|
132
149
|
// SafeAreaInsets.toMap()
|
|
133
150
|
// ============================================================
|
package/dist/otel.js
CHANGED
|
@@ -294,33 +294,6 @@ export function withObservability(serviceName, fn, name) {
|
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
/**
|
|
298
|
-
* Creates a recorder for out-of-band response stream errors.
|
|
299
|
-
*
|
|
300
|
-
* Stream errors (e.g. Node's ERR_STREAM_WRITE_AFTER_END) surface on the
|
|
301
|
-
* response object's "error" event, typically after the span that performed
|
|
302
|
-
* the offending write has already ended, so recording the exception on the
|
|
303
|
-
* active span at error time would be silently dropped. Instead, capture the
|
|
304
|
-
* ambient context while the request span is still active and emit a dedicated
|
|
305
|
-
* error span into the same trace for each stream error (the same parenting
|
|
306
|
-
* approach responseFlushMiddleware uses for its post-end spans).
|
|
307
|
-
*
|
|
308
|
-
* @param {string} serviceName - The name of the service
|
|
309
|
-
* @param {string} name - Span name (optional)
|
|
310
|
-
* @returns {Function} (error) => void, safe to call after the request span ended
|
|
311
|
-
*/
|
|
312
|
-
export function createStreamErrorRecorder(serviceName, name = "responseStreamError") {
|
|
313
|
-
const tracer = trace.getTracer(serviceName)
|
|
314
|
-
const parentContext = context.active()
|
|
315
|
-
return function recordStreamError(error) {
|
|
316
|
-
const span = tracer.startSpan(name, undefined, parentContext)
|
|
317
|
-
if (error && error.code) span.setAttribute("error.type", String(error.code))
|
|
318
|
-
span.recordException(error)
|
|
319
|
-
span.setStatus({ code: 2, message: error && error.message ? error.message : String(error) })
|
|
320
|
-
span.end()
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
297
|
/**
|
|
325
298
|
* Express middleware that emits two spans describing what happens to the
|
|
326
299
|
* response body AFTER the app calls res.end() — the work that lives past the
|
|
@@ -425,10 +398,4 @@ export function responseFlushMiddleware(
|
|
|
425
398
|
}
|
|
426
399
|
}
|
|
427
400
|
|
|
428
|
-
export default {
|
|
429
|
-
init,
|
|
430
|
-
withObservability,
|
|
431
|
-
withSyncObservability,
|
|
432
|
-
responseFlushMiddleware,
|
|
433
|
-
createStreamErrorRecorder,
|
|
434
|
-
}
|
|
401
|
+
export default { init, withObservability, withSyncObservability, responseFlushMiddleware }
|
|
@@ -102,7 +102,6 @@ try {
|
|
|
102
102
|
// Passthrough no-ops used when OTEL_ENABLE is not set; replaced below if enabled.
|
|
103
103
|
let withObservability = (_service, fn) => fn;
|
|
104
104
|
let withSyncObservability = (_service, fn) => fn;
|
|
105
|
-
let createStreamErrorRecorder = null;
|
|
106
105
|
|
|
107
106
|
// config.json booleans survive the process.env swap in loadEnvironmentVariables(),
|
|
108
107
|
// so this is genuinely true at runtime when config sets OTEL_ENABLE: true. The cast
|
|
@@ -112,7 +111,6 @@ if (process.env.OTEL_ENABLE === true) {
|
|
|
112
111
|
const otel = await import("../../otel.js");
|
|
113
112
|
withObservability = otel.withObservability;
|
|
114
113
|
withSyncObservability = otel.withSyncObservability;
|
|
115
|
-
createStreamErrorRecorder = otel.createStreamErrorRecorder;
|
|
116
114
|
} catch {
|
|
117
115
|
// otel packages not installed — continue without tracing
|
|
118
116
|
}
|
|
@@ -168,26 +166,6 @@ const getComponent = (store, context, req, fetcherData, isBot) => <div id="app">
|
|
|
168
166
|
</div>;
|
|
169
167
|
|
|
170
168
|
// ── Render and stream ──────────────────────────────────────────────────
|
|
171
|
-
// Responses that already have a stream-error listener attached. Guarded via
|
|
172
|
-
// WeakSet because renderMarkUp can run twice for one response (the fetcher
|
|
173
|
-
// error path re-renders with a 404) and "error" listeners must not stack.
|
|
174
|
-
const _observedResponses = new WeakSet();
|
|
175
|
-
|
|
176
|
-
// Node swallows response stream errors (e.g. ERR_STREAM_WRITE_AFTER_END) when
|
|
177
|
-
// res has no "error" listener: the write is dropped, nothing is logged, and a
|
|
178
|
-
// truncated document ships silently. Log every stream error through
|
|
179
|
-
// logSSRError (RUNTIME-WEB-001, like every other SSR failure) and, when tracing
|
|
180
|
-
// is enabled, record it as an error span in the request's trace. Must be
|
|
181
|
-
// called while the request span is still active so the span parents correctly.
|
|
182
|
-
const observeResponseStreamErrors = res => {
|
|
183
|
-
if (_observedResponses.has(res)) return;
|
|
184
|
-
_observedResponses.add(res);
|
|
185
|
-
const recordStreamError = createStreamErrorRecorder ? createStreamErrorRecorder(SSR_SERVICE) : null;
|
|
186
|
-
res.on("error", error => {
|
|
187
|
-
logSSRError("RENDER", error);
|
|
188
|
-
if (recordStreamError) recordStreamError(error);
|
|
189
|
-
});
|
|
190
|
-
};
|
|
191
169
|
const _renderMarkUp = async (errorCode, req, res, metaTags, fetcherData, store, allMatches, context, chunkExtractor) => {
|
|
192
170
|
const deviceDetails = getUserAgentDetails(req.headers["user-agent"] || "");
|
|
193
171
|
// Match mweb's wider definition: synthetic monitors (StatusCake) and AI crawlers
|
|
@@ -269,7 +247,6 @@ const _renderMarkUp = async (errorCode, req, res, metaTags, fetcherData, store,
|
|
|
269
247
|
"content-type": "text/html; charset=utf-8"
|
|
270
248
|
});
|
|
271
249
|
res.status(status);
|
|
272
|
-
observeResponseStreamErrors(res);
|
|
273
250
|
return new Promise((resolve, reject) => {
|
|
274
251
|
// Single completion path: React's pipe() auto-ends `tail`, and
|
|
275
252
|
// `flush()` appends the deferred asset tags before that end
|
|
@@ -74,7 +74,7 @@ const createClientConfig = async () => {
|
|
|
74
74
|
return undefined
|
|
75
75
|
}
|
|
76
76
|
if (
|
|
77
|
-
/[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/.test(
|
|
77
|
+
/[\\/]node_modules[\\/](react|react-dom|scheduler|react-fast-compare|react-side-effect|react-helmet-async)[\\/]/.test(
|
|
78
78
|
id
|
|
79
79
|
)
|
|
80
80
|
) {
|
package/dist/vite/vite.config.js
CHANGED
|
@@ -266,7 +266,9 @@ const browserOptimizeDeps = [
|
|
|
266
266
|
"redux",
|
|
267
267
|
"redux-thunk",
|
|
268
268
|
"axios",
|
|
269
|
+
"react-helmet-async",
|
|
269
270
|
"react-google-recaptcha",
|
|
271
|
+
"react-fast-compare",
|
|
270
272
|
"history",
|
|
271
273
|
"lottie-web",
|
|
272
274
|
// Pre-bundle alongside React so esbuild marks react/react-router as external
|
|
@@ -293,6 +295,7 @@ const browserOptimizeDeps = [
|
|
|
293
295
|
// first navigation that pulls them in doesn't trigger a full-page reload.
|
|
294
296
|
"react-loadable-visibility",
|
|
295
297
|
"react-detect-offline",
|
|
298
|
+
"react-side-effect",
|
|
296
299
|
"react-async-script",
|
|
297
300
|
"normalize.css",
|
|
298
301
|
]
|
|
@@ -354,6 +357,7 @@ export const sharedViteConfig = {
|
|
|
354
357
|
optimizeDeps: {
|
|
355
358
|
include: [
|
|
356
359
|
"invariant",
|
|
360
|
+
"react-fast-compare",
|
|
357
361
|
"shallowequal",
|
|
358
362
|
"prop-types",
|
|
359
363
|
"redux-thunk",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, { useContext, useState, useEffect } from "react";
|
|
2
|
+
import { HelmetProvider, Helmet } from "react-helmet-async";
|
|
2
3
|
import { OneMgRouterContext } from "../context.jsx";
|
|
3
4
|
import { useRouterData } from "./RouterDataProvider.jsx";
|
|
4
|
-
import { getMetaData } from "../utils/metaDataUtils.jsx";
|
|
5
|
+
import { deleteHeadTagsByDataAttribute, getMetaData } from "../utils/metaDataUtils.jsx";
|
|
5
6
|
import { useLocation } from "react-router";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -13,13 +14,19 @@ export const MetaTag = () => {
|
|
|
13
14
|
} = useContext(OneMgRouterContext);
|
|
14
15
|
const routeData = useRouterData();
|
|
15
16
|
const location = useLocation();
|
|
16
|
-
const [metaTags, setMetaTags] = useState([]);
|
|
17
|
+
const [metaTags, setMetaTags] = useState([<meta key={0}></meta>]);
|
|
17
18
|
useEffect(() => {
|
|
18
19
|
const mergedMetaTags = getMetaData(matchedRoutes, routeData);
|
|
19
|
-
|
|
20
|
+
if (Array.isArray(mergedMetaTags) && mergedMetaTags.length > 0) {
|
|
21
|
+
setMetaTags(mergedMetaTags);
|
|
22
|
+
}
|
|
23
|
+
return () => deleteHeadTagsByDataAttribute("catalyst");
|
|
20
24
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
21
25
|
}, [location]);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
if (Array.isArray(metaTags) && metaTags.length >= 0) {
|
|
27
|
+
return <HelmetProvider>
|
|
28
|
+
<Helmet>{metaTags}</Helmet>
|
|
29
|
+
</HelmetProvider>;
|
|
30
|
+
}
|
|
31
|
+
return <></>;
|
|
25
32
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react"
|
|
2
|
-
import { describe, expect, it, vi } from "vitest"
|
|
2
|
+
import { describe, expect, it, vi, afterEach } from "vitest"
|
|
3
3
|
import { render, waitFor, act } from "@testing-library/react"
|
|
4
|
-
import { MemoryRouter, Routes, Route,
|
|
4
|
+
import { MemoryRouter, Routes, Route, useNavigate } from "react-router"
|
|
5
5
|
import { MetaTag } from "./MetaTag.jsx"
|
|
6
6
|
import { OneMgRouterContext } from "../context.jsx"
|
|
7
7
|
import { RouterContext } from "./RouterDataProvider.jsx"
|
|
@@ -26,6 +26,10 @@ function renderMetaTag({ matchedRoutes = [], routerData = {} } = {}) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
describe("MetaTag", () => {
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
document.head.innerHTML = ""
|
|
31
|
+
})
|
|
32
|
+
|
|
29
33
|
it("renders without crashing when there are no matched routes", () => {
|
|
30
34
|
expect(() => renderMetaTag()).not.toThrow()
|
|
31
35
|
})
|
|
@@ -44,64 +48,53 @@ describe("MetaTag", () => {
|
|
|
44
48
|
await waitFor(() => {
|
|
45
49
|
expect(document.head.querySelector('meta[name="description"]')).not.toBeNull()
|
|
46
50
|
})
|
|
47
|
-
expect(document.head.querySelector('meta[name="description"]').getAttribute("content")).toBe(
|
|
51
|
+
expect(document.head.querySelector('meta[name="description"]').getAttribute("content")).toBe(
|
|
52
|
+
"hello"
|
|
53
|
+
)
|
|
48
54
|
})
|
|
49
55
|
|
|
50
|
-
it("
|
|
56
|
+
it("keeps the placeholder meta tag when no route provides setMetaData", () => {
|
|
51
57
|
renderMetaTag({ matchedRoutes: [{ route: { component: {} } }] })
|
|
52
|
-
|
|
58
|
+
// Component should still render (Helmet/HelmetProvider tree),
|
|
59
|
+
// even with the single empty placeholder <meta> from useState.
|
|
60
|
+
expect(document.querySelector("head")).not.toBeNull()
|
|
53
61
|
})
|
|
54
62
|
|
|
55
|
-
it("clears previous
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
]
|
|
65
|
-
|
|
66
|
-
function RouteMetadata() {
|
|
67
|
-
const location = useLocation()
|
|
68
|
-
const matchedRoutes = location.pathname === "/with-metadata" ? routeWithMetadata : []
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<OneMgRouterContext.Provider value={{ matchedRoutes }}>
|
|
72
|
-
<RouterContext.Provider value={{}}>
|
|
73
|
-
<MetaTag />
|
|
74
|
-
</RouterContext.Provider>
|
|
75
|
-
</OneMgRouterContext.Provider>
|
|
76
|
-
)
|
|
77
|
-
}
|
|
63
|
+
it("re-runs setMetaData and clears previous tags when the location changes", async () => {
|
|
64
|
+
// A rerender() with a NEW <MemoryRouter initialEntries> does not
|
|
65
|
+
// navigate an already-mounted router -- initialEntries is only
|
|
66
|
+
// read on first mount. Confirmed directly: that approach left
|
|
67
|
+
// setMetaData called once even after "changing" the path.
|
|
68
|
+
// Navigating within one mounted router (via a real useNavigate
|
|
69
|
+
// call, triggered here through a button) is what actually
|
|
70
|
+
// changes useLocation() and re-fires MetaTag's effect.
|
|
71
|
+
const setMetaData = vi.fn(() => [<meta key="d" name="description" content="v1" />])
|
|
72
|
+
const matchedRoutes = [{ route: { component: { setMetaData } } }]
|
|
78
73
|
|
|
79
74
|
function NavButton() {
|
|
80
75
|
const navigate = useNavigate()
|
|
81
76
|
return (
|
|
82
|
-
<button type="button" onClick={() => navigate("/
|
|
77
|
+
<button type="button" onClick={() => navigate("/b")}>
|
|
83
78
|
go
|
|
84
79
|
</button>
|
|
85
80
|
)
|
|
86
81
|
}
|
|
87
82
|
|
|
88
83
|
render(
|
|
89
|
-
<MemoryRouter initialEntries={["/
|
|
90
|
-
<
|
|
91
|
-
|
|
84
|
+
<MemoryRouter initialEntries={["/a"]}>
|
|
85
|
+
<OneMgRouterContext.Provider value={{ matchedRoutes }}>
|
|
86
|
+
<RouterContext.Provider value={{}}>
|
|
87
|
+
<NavButton />
|
|
88
|
+
<MetaTag />
|
|
89
|
+
</RouterContext.Provider>
|
|
90
|
+
</OneMgRouterContext.Provider>
|
|
92
91
|
</MemoryRouter>
|
|
93
92
|
)
|
|
94
|
-
|
|
95
|
-
await waitFor(() => {
|
|
96
|
-
expect(document.head.querySelector('meta[name="description"]')).not.toBeNull()
|
|
97
|
-
})
|
|
93
|
+
await waitFor(() => expect(setMetaData).toHaveBeenCalledTimes(1))
|
|
98
94
|
|
|
99
95
|
await act(async () => {
|
|
100
96
|
document.querySelector("button").click()
|
|
101
97
|
})
|
|
102
|
-
|
|
103
|
-
await waitFor(() => {
|
|
104
|
-
expect(document.head.querySelector('meta[name="description"]')).toBeNull()
|
|
105
|
-
})
|
|
98
|
+
await waitFor(() => expect(setMetaData).toHaveBeenCalledTimes(2))
|
|
106
99
|
})
|
|
107
100
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "catalyst-core",
|
|
3
|
-
"version": "1.0.1
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.jsx",
|
|
6
6
|
"description": "Web framework that provides great performance out of the box",
|
|
@@ -72,12 +72,13 @@
|
|
|
72
72
|
"@vitejs/plugin-react": "^4.4.1",
|
|
73
73
|
"ansis": "^4.3.1",
|
|
74
74
|
"body-parser": "^1.20.2",
|
|
75
|
-
"compression": "^1.
|
|
75
|
+
"compression": "^1.8.2",
|
|
76
76
|
"cookie-parser": "^1.4.6",
|
|
77
77
|
"express": "^4.22.3",
|
|
78
78
|
"express-static-gzip": "^2.1.7",
|
|
79
79
|
"react": "19.0.0",
|
|
80
80
|
"react-dom": "19.0.0",
|
|
81
|
+
"react-helmet-async": "^1.3.0",
|
|
81
82
|
"react-redux": "^9.2.0",
|
|
82
83
|
"react-refresh": "^0.14.0",
|
|
83
84
|
"redux": "^4.2.1",
|
|
@@ -136,6 +137,10 @@
|
|
|
136
137
|
]
|
|
137
138
|
},
|
|
138
139
|
"overrides": {
|
|
140
|
+
"react-helmet-async": {
|
|
141
|
+
"react": "19.0.0",
|
|
142
|
+
"react-dom": "19.0.0"
|
|
143
|
+
},
|
|
139
144
|
"react-redux": {
|
|
140
145
|
"react": "19.0.0",
|
|
141
146
|
"react-dom": "19.0.0"
|
|
@@ -160,33 +160,6 @@ describe("SSR handler", () => {
|
|
|
160
160
|
expect(res.getHtml()).toContain("<html")
|
|
161
161
|
})
|
|
162
162
|
|
|
163
|
-
it("logs a response stream error as RUNTIME-WEB-001 instead of swallowing it", async () => {
|
|
164
|
-
const { default: handler } = await import("../../src/server/renderer/handler.jsx")
|
|
165
|
-
const { req, res } = makeReqRes("/")
|
|
166
|
-
|
|
167
|
-
await handler(req, res)
|
|
168
|
-
await res.waitForEnd()
|
|
169
|
-
|
|
170
|
-
const streamError: any = new Error("write after end")
|
|
171
|
-
streamError.code = "ERR_STREAM_WRITE_AFTER_END"
|
|
172
|
-
res.emit("error", streamError)
|
|
173
|
-
|
|
174
|
-
const logged = (console.error as any).mock.calls.flat().join("\n")
|
|
175
|
-
expect(logged).toContain("RUNTIME-WEB-001")
|
|
176
|
-
expect(logged).toContain("write after end")
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it("attaches one stream-error listener even when the fetcher-error path renders twice", async () => {
|
|
180
|
-
const { default: handler } = await import("../../src/server/renderer/handler.jsx")
|
|
181
|
-
const { req, res } = makeReqRes("/fetcher-error")
|
|
182
|
-
const before = res.listenerCount("error")
|
|
183
|
-
|
|
184
|
-
await handler(req, res)
|
|
185
|
-
await res.waitForEnd()
|
|
186
|
-
|
|
187
|
-
expect(res.listenerCount("error")).toBe(before + 1)
|
|
188
|
-
})
|
|
189
|
-
|
|
190
163
|
it("fails the request with 500 (no render) when configureStore is not a function", async () => {
|
|
191
164
|
// handler.jsx imports createStore as a module-scope default binding,
|
|
192
165
|
// so swap the fixture module before the fresh import. A non-function
|