Workers P2: Fehlergrenze um die Routen, damit ein fehlender lazy-Chunk nicht die ganze Oberfläche leert
Ein alter Tab nach einem Deploy fordert den Seiten-Chunk mit altem Hash an, nginx liefert die index.html, der Import schlägt fehl und React räumte ohne Boundary den gesamten Baum ab. RouteErrorBoundary hält Menü und Rahmen, zeigt den Hinweis mit Knopf „Seite neu laden“ und räumt den Fehler beim Pfadwechsel weg. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7ddd60e59b
commit
0b78379b9b
4 files changed
+119
-1
No files matched your search
@@ -0,0 +1,65 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import esbuild from 'esbuild'
|
||||
|
||||
// Die Boundary ist JSX mit React: gebündelt wird mit eigenem automatischem JSX-Laufzeitpaket (load.mjs kennt kein JSX).
|
||||
// Gezeichnet wird mit renderToStaticMarkup; der Fehlerzustand kommt über eine Unterklasse, die ihn vorab setzt
|
||||
// (der Server-Renderer selbst fängt keine Fehler in Boundaries ab).
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
|
||||
async function load() {
|
||||
const result = await esbuild.build({
|
||||
stdin: {
|
||||
contents: [
|
||||
"import { createElement } from 'react'",
|
||||
"import { renderToStaticMarkup } from 'react-dom/server'",
|
||||
"import Boundary from './src/components/RouteErrorBoundary.jsx'",
|
||||
'export { createElement, renderToStaticMarkup, Boundary }',
|
||||
].join('\n'),
|
||||
resolveDir: repoRoot,
|
||||
loader: 'js',
|
||||
},
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
platform: 'browser',
|
||||
jsx: 'automatic',
|
||||
define: { 'process.env.NODE_ENV': '"production"' },
|
||||
write: false,
|
||||
})
|
||||
const code = result.outputFiles[0].text
|
||||
return import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`)
|
||||
}
|
||||
|
||||
test('RouteErrorBoundary: ohne Fehler bleiben die Kinder unverändert', async () => {
|
||||
const { createElement, renderToStaticMarkup, Boundary } = await load()
|
||||
const html = renderToStaticMarkup(createElement(Boundary, { resetKey: '/hustler' }, createElement('p', null, 'Inhalt')))
|
||||
assert.equal(html, '<p>Inhalt</p>')
|
||||
})
|
||||
|
||||
test('RouteErrorBoundary: im Fehlerfall Hinweis mit Knopf „Seite neu laden“ statt der Kinder, als Alarm ausgezeichnet', async () => {
|
||||
const { createElement, renderToStaticMarkup, Boundary } = await load()
|
||||
class Probe extends Boundary {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { error: new TypeError('Failed to fetch dynamically imported module'), resetKey: props.resetKey }
|
||||
}
|
||||
}
|
||||
const html = renderToStaticMarkup(createElement(Probe, { resetKey: '/hustler' }, createElement('p', null, 'Inhalt')))
|
||||
assert.doesNotMatch(html, /Inhalt/)
|
||||
assert.match(html, /role="alert"/)
|
||||
assert.match(html, /Diese Seite konnte nicht angezeigt werden\./)
|
||||
assert.match(html, /<button[^>]*type="button"[^>]*>Seite neu laden<\/button>/)
|
||||
assert.match(html, /neue Version von WOMS/)
|
||||
})
|
||||
|
||||
test('RouteErrorBoundary: getDerivedStateFromError merkt den Fehler, ein Pfadwechsel räumt ihn weg, gleicher Pfad nicht', async () => {
|
||||
const { Boundary } = await load()
|
||||
const err = new TypeError('Failed to fetch dynamically imported module')
|
||||
assert.deepEqual(Boundary.getDerivedStateFromError(err), { error: err })
|
||||
const stuck = { error: err, resetKey: '/hustler' }
|
||||
assert.equal(Boundary.getDerivedStateFromProps({ resetKey: '/hustler' }, stuck), null)
|
||||
assert.deepEqual(Boundary.getDerivedStateFromProps({ resetKey: '/tickets' }, stuck), { error: null, resetKey: '/tickets' })
|
||||
assert.deepEqual(Boundary.getDerivedStateFromProps({ resetKey: '/tickets' }, { error: null, resetKey: '/hustler' }), { error: null, resetKey: '/tickets' })
|
||||
})
|
||||
@@ -17,3 +17,12 @@ test('WorkersPage: Titel Hustler (H1 und Tab-Titel), Reiter hochhaus (kein Platz
|
||||
assert.match(s, /id: 'hochhaus'/); assert.doesNotMatch(s, /id: 'freigabe'/); assert.match(s, /WORKERS_TABS\.length > 1/)
|
||||
assert.match(s, /const api = workersApi/); assert.match(s, /pickTab\(/); assert.match(s, /isAdmin=\{isAdmin\}/)
|
||||
})
|
||||
test('App: RouteErrorBoundary um die Routen im Inhaltsbereich (fehlender lazy-Chunk in altem Tab lässt nicht die ganze Oberfläche verschwinden), Pfad als resetKey', () => {
|
||||
const s = readFileSync(new URL('../../src/App.jsx', import.meta.url), 'utf8')
|
||||
assert.match(s, /import RouteErrorBoundary from '\.\/components\/RouteErrorBoundary'/)
|
||||
assert.match(s, /<main id="inhalt"><RouteErrorBoundary resetKey=\{location\.pathname\}><AppRoutes \/><\/RouteErrorBoundary><\/main>/)
|
||||
// das Menü (Sidebar) steht außerhalb der Boundary und bleibt bei einem Fehler benutzbar
|
||||
assert.ok(s.indexOf('<Sidebar') < s.indexOf('<RouteErrorBoundary'))
|
||||
const b = readFileSync(new URL('../../src/components/RouteErrorBoundary.jsx', import.meta.url), 'utf8')
|
||||
assert.match(b, /window\.location\.reload\(\)/); assert.match(b, /Seite neu laden/); assert.match(b, /getDerivedStateFromError/)
|
||||
})
|
||||
Reference in new issue
Block a user