Skip to content

Commit b3de5a6

Browse files
authored
feat: updated error page (#664)
* test: ensure ipfs-gateway loads all car fixtures * test: image loading smoke test * test: dir redirect test * test: add swResponses test fixture * test: validate dir-index-html text and links * tmp * chore: add ipns-record fixture * fix: all tests pass with updated vfetch * chore: fix lint error * chore: update verified-fetch to latest * chore: update package-lock.json after reset
1 parent 49fe12d commit b3de5a6

11 files changed

+2266
-1351
lines changed

package-lock.json

Lines changed: 2063 additions & 1332 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,24 +51,24 @@
5151
"@helia/block-brokers": "^4.1.0",
5252
"@helia/delegated-routing-v1-http-api-client": "^4.2.2",
5353
"@helia/http": "^2.0.5",
54-
"@helia/interface": "^5.0.0",
55-
"@helia/routers": "^3.0.0",
56-
"@helia/verified-fetch": "^2.6.4",
57-
"@libp2p/crypto": "^5.0.15",
58-
"@libp2p/dcutr": "^2.0.26",
59-
"@libp2p/identify": "^3.0.26",
60-
"@libp2p/keychain": "^5.1.4",
61-
"@libp2p/logger": "^5.1.3",
62-
"@libp2p/ping": "^2.0.26",
63-
"@libp2p/websockets": "^9.2.7",
64-
"@libp2p/webtransport": "^5.0.36",
54+
"@helia/interface": "^5.2.1",
55+
"@helia/routers": "^3.0.1",
56+
"@helia/verified-fetch": "^2.6.5",
57+
"@libp2p/crypto": "^5.1.0",
58+
"@libp2p/dcutr": "^2.0.28",
59+
"@libp2p/identify": "^3.0.28",
60+
"@libp2p/keychain": "^5.2.0",
61+
"@libp2p/logger": "^5.1.14",
62+
"@libp2p/ping": "^2.0.28",
63+
"@libp2p/websockets": "^9.2.9",
64+
"@libp2p/webtransport": "^5.0.38",
6565
"@multiformats/dns": "^1.0.6",
6666
"@noble/hashes": "^1.5.0",
6767
"execa": "^9.5.2",
6868
"helia": "^5.3.0",
6969
"ipfs-css": "^1.4.0",
7070
"ipfsd-ctl": "^15.0.2",
71-
"libp2p": "^2.8.1",
71+
"libp2p": "^2.8.3",
7272
"multiformats": "^13.3.2",
7373
"react": "^19.0.0",
7474
"react-dom": "^19.0.0",

playwright.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export default defineConfig({
8585
// need to use built assets due to service worker loading issue.
8686
command: process.env.SHOULD_BUILD !== 'false' ? 'npm run build && npx http-server --silent -p 3000 dist' : 'npx http-server --silent -p 3000 dist',
8787
port: 3000,
88-
timeout: 15 * 1000,
88+
timeout: 60 * 1000,
8989
reuseExistingServer: !process.env.CI,
9090
stdout: process.env.CI ? undefined : 'pipe',
9191
stderr: process.env.CI ? undefined : 'pipe'

src/sw.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,7 @@ async function fetchHandler ({ path, request, event }: FetchHandlerArg): Promise
538538
log.error('fetchHandler: response not ok: ', response)
539539
return await errorPageResponse(response)
540540
}
541+
response.headers.set('ipfs-sw', 'true')
541542
return response
542543
} catch (err: unknown) {
543544
const errorMessages: string[] = []
@@ -585,6 +586,8 @@ async function errorPageResponse (fetchResponse: Response): Promise<Response> {
585586
}
586587
if (json == null) {
587588
json = { error: { message: `${fetchResponse.statusText}: ${responseBodyAsText}`, stack: null } }
589+
} else if (json.error == null) {
590+
json.error = { message: json.errors.map(e => `<li>${e.message}</li>`).join(''), stack: json.errors.map(e => `<li>${e.stack}</li>`).join('\n') }
588591
}
589592

590593
const responseDetails = getResponseDetails(fetchResponse, responseBodyAsText)
@@ -656,6 +659,14 @@ function getResponseDetails (response: Response, responseBody: string): Response
656659
headers[key] = value
657660
})
658661

662+
if (response.headers.get('Content-Type')?.includes('application/json') === true) {
663+
try {
664+
responseBody = JSON.parse(responseBody)
665+
} catch (e) {
666+
log.error('error parsing json for error page response', e)
667+
}
668+
}
669+
659670
return {
660671
responseBody,
661672
headers,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { Page, Response } from '@playwright/test'
2+
3+
/**
4+
* Yields all the responses from the service worker.
5+
*
6+
* You must abort the signal to stop the capture.
7+
*/
8+
export async function * captureAllSwResponses (page: Page, signal: AbortSignal): AsyncGenerator<Response> {
9+
const responseQueue: Response[] = []
10+
let resolveNext: ((value: Response) => void) | null = null
11+
12+
// Helper function to get the next response
13+
const getNextResponse = async (): Promise<Response> => {
14+
const response = responseQueue.shift()
15+
if (response != null) {
16+
return Promise.resolve(response)
17+
}
18+
return new Promise(resolve => {
19+
resolveNext = resolve
20+
})
21+
}
22+
23+
// Set up the response listener
24+
const onResponse = (response: Response): void => {
25+
if (response.headers()['ipfs-sw'] !== 'true') {
26+
return
27+
}
28+
if (resolveNext != null) {
29+
resolveNext(response)
30+
resolveNext = null
31+
} else {
32+
responseQueue.push(response)
33+
}
34+
}
35+
36+
page.on('response', onResponse)
37+
38+
try {
39+
while (!signal.aborted) {
40+
const response = await getNextResponse()
41+
if (signal.aborted) break
42+
yield response
43+
}
44+
} finally {
45+
page.off('response', onResponse)
46+
}
47+
}

test-e2e/fixtures/config-test-fixtures.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { test as base } from '@playwright/test'
1+
import { test as base, type Response } from '@playwright/test'
2+
import { captureAllSwResponses } from './capture-all-sw-responses.js'
23
import { setConfig } from './set-sw-config.js'
34
import { waitForServiceWorker } from './wait-for-service-worker.js'
45

@@ -15,9 +16,37 @@ const baseURLProtocol = async ({ baseURL }, use): Promise<void> => {
1516
await use(url.protocol)
1617
}
1718

18-
export const test = base.extend<{ rootDomain: string, baseURL: string, protocol: string }>({
19+
/**
20+
* A fixture that captures all the responses from the service worker.
21+
*/
22+
const swResponses = async ({ page }, use): Promise<void> => {
23+
const capturedResponses: Response[] = []
24+
const controller = new AbortController()
25+
const signal = controller.signal;
26+
27+
// background capture
28+
(async () => {
29+
try {
30+
for await (const response of captureAllSwResponses(page, signal)) {
31+
capturedResponses.push(response)
32+
}
33+
} catch (err) {
34+
// do not kill the test runner, just log the error
35+
// eslint-disable-next-line no-console
36+
console.error('Error in SW capture:', err)
37+
}
38+
})().catch(() => {})
39+
40+
await use(capturedResponses)
41+
42+
// stop the capture loop because the test is done
43+
controller.abort()
44+
}
45+
46+
export const test = base.extend<{ rootDomain: string, baseURL: string, protocol: string, swResponses: Response[] }>({
1947
rootDomain: [rootDomain, { scope: 'test' }],
2048
protocol: [baseURLProtocol, { scope: 'test' }],
49+
swResponses,
2150
page: async ({ page }, use) => {
2251
if (isNoServiceWorkerProject(test)) {
2352
test.skip()
117 KB
Binary file not shown.
394 Bytes
Binary file not shown.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { waitForServiceWorker } from './wait-for-service-worker.js'
2+
import type { Page, Response } from '@playwright/test'
3+
4+
/**
5+
* Navigates to a URL and returns the last response from the service worker.
6+
*/
7+
export async function navigateAndGetSwResponse (page: Page, { url, swScope }: { url: string, swScope: string }): Promise<Response> {
8+
const swScopeUrl = new URL(swScope)
9+
const lastSwResponsePromise = page.waitForResponse(response => {
10+
// if firefox is being used, .fromServiceWorker() will return false, so we need to check something else instead
11+
return response.url().includes(swScopeUrl.host) && response.headers()['ipfs-sw'] === 'true'
12+
})
13+
14+
await page.goto(url)
15+
16+
await waitForServiceWorker(page, swScope)
17+
18+
return lastSwResponsePromise
19+
}
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import type { Page } from '@playwright/test'
22

3-
export async function waitForServiceWorker (page: Page): Promise<void> {
4-
await page.waitForFunction(async () => {
3+
export async function waitForServiceWorker (page: Page, scope?: string): Promise<void> {
4+
await page.waitForFunction(async ({ scope }: { scope?: string }) => {
55
const registration = await window.navigator.serviceWorker.getRegistration()
66

77
if (registration?.active?.state === 'activated') {
8-
return true
8+
if (scope != null) {
9+
// expectedScope is provided, so we need to check if the scope is as expected
10+
return registration?.scope === scope
11+
} else {
12+
// no expectedScope, so we just need to check if the service worker is activated
13+
return true
14+
}
915
}
10-
})
16+
}, { scope })
1117
}

0 commit comments

Comments
 (0)