## 経緯
[[Bruno]]には[[テスト結果をHTMLレポートで出力 (Bruno)|テスト結果をHTMLレポートで出力]]する機能があるが、実際に利用していて気になる点がいくつかある。
- 結果のアコーディオン形式
- サイドバーに概要一覧、メインに詳細のほうが分かりやすい
- 横幅が短いディスプレイでも見やすいようにだと思うけど...
- リクエストの概要情報に縦幅をとる
- よく確認する項目以外はデフォルト非表示でいい
- レスポンスの中身が最初からすべて表示されている
- 最初の10行くらい表示して、あとは展開でいい
- 結果内の表示順
- 以下のようにしたい
1. 何に失敗したか?
2. リクエストの概要
3. レスポンスの概要
- 失敗したテストケースと差分概要を一覧できる機能がほしい
## 要件
- [[テスト結果をJSONレポートで出力 (Bruno)|テスト結果をJSONレポートで出力]] (`resport.json`) し、それを参照して動く
- local serverを起動する必要がありそう
- [[Raycast]]でテスト実行後に、終了したら自動でブラウザが起動すること
- ターミナルコマンドで起動できるものが必要
- `open http://localhost:3000` とかでいけそう
## 目的
今回の目的は2点。
- [[Bruno]]のレポートを見やすくしたい
- 特に仕事で利用したい
- 仕事で開発している技術スタックに関する学びを得たい
新しい技術を試すというよりは、仕事で利用する技術を補完的に学習できる環境が欲しいというポジション。
## インターフェース
こんな雰囲気
```bash
# テスト実行
bru run --reporter-json report.json
./bruno-report-viewer show report.json
```
## 技術スタック
[[#目的]]により、仕事で主に使っている技術スタックを利用する。
- [[Vue3]]
- [[Vite]]
- [[Rolldown]]
- [[Tailwind CSS]] v4
- [[shadcn-vue]]
### 環境
| 対象 | バージョン |
| ---------------------------- | ------ |
| [[macOS]] | 15.7.2 |
| [[Bun]] | 1.3.2 |
| [[Vue]] | 3.5.24 |
| [[rolldown-vite]] | 7.2.5 |
| [[Tailwind CSS]] | 4.1.17 |
| [[TypeScript]] | 5.9.3 |
| [[Reka UI]] | 2.6.0 |
| [[Class Variance Authority]] | 0.7.1 |
## プロジェクト環境構築
```console
$ bun create vite@latest bruno-report-viewer --template vue-ts
│
◇ Use rolldown-vite (Experimental)?:
│ Yes
│
◇ Install with bun and start now?
│ Yes
│
◇ Scaffolding project in /Users/tadashi-aikawa/git/github.com/tadashi-aikawa/bruno-report-viewer...
│
◇ Installing dependencies with bun...
bun install v1.3.2 (b131639c)
+ @types/
[email protected]
+ @vitejs/
[email protected]
+ @vue/
[email protected]
+
[email protected]
+
[email protected]
+
[email protected]
+
[email protected]
50 packages installed [4.76s]
```
```console
$ cd bruno-report-viewer
$ bun add tailwindcss @tailwindcss/vite
bun add v1.3.2 (b131639c)
installed
[email protected]
installed @tailwindcss/
[email protected]
13 packages installed [3.55s]
```
`src/style.css`
```css
@import "tailwindcss";
```
`tsconfig.json`
```json
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
```
`tsconfig.app.json`
```json
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
```
```console
$ bun add -D @types/node
bun add v1.3.2 (b131639c)
installed @types/
[email protected]
```
`vite.config.ts`
```ts
import path from "node:path";
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});
```
```console
$ bunx --bun shadcn-vue@latest init
✔ Preflight checks.
✔ Verifying framework. Found Vite.
✔ Validating Tailwind CSS config. Found v4.
✔ Validating import alias.
✔ Which color would you like to use as the base color? › Neutral
✔ Writing components.json.
✔ Checking registry.
✔ Updating CSS variables in src/style.css
✔ Installing dependencies.
✔ Created 1 file:
- src/lib/utils.ts
```
### 動作確認
```console
bunx --bun shadcn-vue@latest add button
```
`src/App.vue`
```html
<script setup lang="ts">
import { Button } from '@/components/ui/button'
</script>
<template>
<Button>Click Me</Button>
</template>
```
### フォーマッター
```console
bun add -D prettier prettier-plugin-organize-imports prettier-plugin-tailwindcss
```
`.prettierrc.json`
```json
{
"plugins": [
"prettier-plugin-organize-imports",
"prettier-plugin-tailwindcss"
],
"tailwindFunctions": ["cva", "cn"]
}
```
### [[TypeScript]]のコードチェックを堅牢化
[[vueCompilerOptions.strictTemplates]] を追加。
`tsconfig.json`
```json
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"vueCompilerOptions": {
"strictTemplates": true
}
}
```
フォールバックを別途定義。
`src/allow-fallthrough-props.d.ts`
```ts
import "vue";
declare module "vue" {
// for vue components
interface AllowedComponentProps {
// inputにフォールスルー属性として利用
type?: unknown;
// LargeInput以外の箇所でフォールスルー属性として利用と仮定
placeholder?: unknown;
// @changeのフォールスルー属性
onChange?: unknown;
// @clickのフォールスルー属性
onClick?: unknown;
[key: `data${Capitalize<string>}`]: string;
// TODO: フォールスルー属性が増えたら追加していく
}
// for native html elements
interface HTMLAttributes {
// allow any data-* attr
[key: `data-${string}`]: string;
}
}
export {};
```
しばらく設定が効かなくてハマった。。
<div class="link-card-v2">
<div class="link-card-v2-site">
<img class="link-card-v2-site-icon" src="https://publish-01.obsidian.md/access/35d05cd1bf5cc500e11cc8ba57daaf88/favicon-64.png" />
<span class="link-card-v2-site-name">Minerva</span>
</div>
<div class="link-card-v2-title">
📝shadcn-vueでtsconfig.jsonに記載した設定が有効にならない
</div>
<div class="link-card-v2-content">Vite、Vue、TypeScript環境でtsconfig.jsonのvueCompilerOptions.strictTemplates設定が有効にならない事象である。原因はreferencesで参照されるtsconfigは設定を継承しないためで、tsconfig.app.jsonにstrictTemplatesを追加する必要がある。</div>
<img class="link-card-v2-image" src="https://publish-01.obsidian.md/access/35d05cd1bf5cc500e11cc8ba57daaf88/Notes/attachments/troubleshooting.webp" />
<a data-href="📝shadcn-vueでtsconfig.jsonに記載した設定が有効にならない" class="internal-link"></a>
</div>
%%[[📝shadcn-vueでtsconfig.jsonに記載した設定が有効にならない]]%%
## レポートを読み込む
[[Bruno CLI]]を実行してレポートを作成し、それを参考に型定義をつくる。
```console
bru run --reporter-json report.json
```
> [!code]- `report.json`
>
> ```json
> [
> {
> "iterationIndex": 0,
> "results": [
> {
> "test": {
> "filename": "01_animalsの取得.bru"
> },
> "request": {
> "method": "GET",
> "url": "http://localhost:8000/animals/",
> "headers": {
> "Authorization": "Basic ********"
> }
> },
> "response": {
> "status": 200,
> "statusText": "OK",
> "headers": {
> "date": "Sun, 23 Nov 2025 02:20:11 GMT",
> "server": "WSGIServer/0.2 CPython/3.13.7",
> "content-type": "application/json",
> "vary": "Accept, Cookie",
> "allow": "GET, POST, HEAD, OPTIONS",
> "x-frame-options": "DENY",
> "content-length": "514",
> "x-content-type-options": "nosniff",
> "referrer-policy": "same-origin",
> "cross-origin-opener-policy": "same-origin"
> },
> "data": [
> {
> "id": "2ebc301b-8f53-4dfc-b0e2-aba69a4149f7",
> "name": "マサハル",
> "description": "我が家の愛犬",
> "proper": false,
> "kind": "dog",
> "created": "2025-11-23T01:44:46.482579Z"
> },
> {
> "id": "81e85f56-806e-4703-9666-496632ea87c5",
> "name": "みたらし",
> "description": "我が家の愛猫",
> "proper": false,
> "kind": "cat",
> "created": "2025-11-23T01:44:46.485263Z"
> },
> {
> "id": "a2cc3173-912c-4dc3-9737-1eb3d53877ef",
> "name": "みみぞう",
> "description": "我が家の愛梟",
> "proper": false,
> "kind": "owl",
> "created": "2025-11-23T01:44:46.486725Z"
> }
> ],
> "url": "http://localhost/animals/",
> "responseTime": 56
> },
> "error": null,
> "status": "pass",
> "assertionResults": [],
> "testResults": [
> {
> "description": "animalsが取得できる",
> "status": "fail",
> "error": "expected 'みたらし' to equal 'みみぞう'",
> "actual": "みたらし",
> "expected": "みみぞう",
> "uid": "e3xwU90r2lSuxf-sl6A8_"
> }
> ],
> "preRequestTestResults": [],
> "postResponseTestResults": [],
> "shouldStopRunnerExecution": false,
> "runDuration": 0.084634792,
> "name": "01_animalsの取得",
> "path": "01_animalsの取得",
> "iterationIndex": 0
> },
> {
> "test": {
> "filename": "02_animalsの登録.bru"
> },
> "request": {
> "method": "POST",
> "url": "http://localhost:8000/animals/",
> "headers": {
> "content-type": "application/json",
> "Authorization": "Basic ********"
> },
> "data": "{\n \"name\": \"みみこ\",\n \"description\": \"みみぞうの妹\",\n \"proper\": false,\n \"kind\": \"owl\"\n}"
> },
> "response": {
> "status": 201,
> "statusText": "Created",
> "headers": {
> "date": "Sun, 23 Nov 2025 02:20:11 GMT",
> "server": "WSGIServer/0.2 CPython/3.13.7",
> "content-type": "application/json",
> "vary": "Accept, Cookie",
> "allow": "GET, POST, HEAD, OPTIONS",
> "x-frame-options": "DENY",
> "content-length": "193",
> "x-content-type-options": "nosniff",
> "referrer-policy": "same-origin",
> "cross-origin-opener-policy": "same-origin"
> },
> "data": {
> "id": "6e01cf59-c8c2-430d-8fcb-a5ee6515aaea",
> "owner": "ネオちゃん",
> "name": "みみこ",
> "description": "みみぞうの妹",
> "proper": false,
> "kind": "owl",
> "created": "2025-11-23T02:20:11.130779Z"
> },
> "url": "http://localhost/animals/",
> "responseTime": 49
> },
> "error": null,
> "status": "pass",
> "assertionResults": [],
> "testResults": [],
> "preRequestTestResults": [],
> "postResponseTestResults": [],
> "shouldStopRunnerExecution": false,
> "runDuration": 0.050211125,
> "name": "02_animalsの登録",
> "path": "02_animalsの登録",
> "iterationIndex": 0
> },
> {
> "test": {
> "filename": "03_animalsの更新.bru"
> },
> "request": {
> "method": "PUT",
> "url": "http://localhost:8000/animals/6e01cf59-c8c2-430d-8fcb-a5ee6515aaea/",
> "headers": {
> "content-type": "application/json",
> "Authorization": "Basic ********"
> },
> "data": "{\n \"name\": \"みみたろう\",\n \"description\": \"みみこの妹\",\n \"proper\": false,\n \"kind\": \"owl\"\n}"
> },
> "response": {
> "status": 200,
> "statusText": "OK",
> "headers": {
> "date": "Sun, 23 Nov 2025 02:20:11 GMT",
> "server": "WSGIServer/0.2 CPython/3.13.7",
> "content-type": "application/json",
> "vary": "Accept, Cookie",
> "allow": "GET, PUT, PATCH, DELETE, HEAD, OPTIONS",
> "x-frame-options": "DENY",
> "content-length": "196",
> "x-content-type-options": "nosniff",
> "referrer-policy": "same-origin",
> "cross-origin-opener-policy": "same-origin"
> },
> "data": {
> "id": "6e01cf59-c8c2-430d-8fcb-a5ee6515aaea",
> "owner": "ネオちゃん",
> "name": "みみたろう",
> "description": "みみこの妹",
> "proper": false,
> "kind": "owl",
> "created": "2025-11-23T02:20:11.130779Z"
> },
> "url": "http://localhost/animals/6e01cf59-c8c2-430d-8fcb-a5ee6515aaea/",
> "responseTime": 49
> },
> "error": null,
> "status": "pass",
> "assertionResults": [],
> "testResults": [],
> "preRequestTestResults": [],
> "postResponseTestResults": [],
> "shouldStopRunnerExecution": false,
> "runDuration": 0.049198542,
> "name": "03_animalsの更新",
> "path": "03_animalsの更新",
> "iterationIndex": 0
> },
> {
> "test": {
> "filename": "04_animalsの削除.bru"
> },
> "request": {
> "method": "DELETE",
> "url": "http://localhost:8000/animals/6e01cf59-c8c2-430d-8fcb-a5ee6515aaea/",
> "headers": {
> "content-type": "application/json",
> "Authorization": "Basic ********"
> }
> },
> "response": {
> "status": 204,
> "statusText": "No Content",
> "headers": {
> "date": "Sun, 23 Nov 2025 02:20:11 GMT",
> "server": "WSGIServer/0.2 CPython/3.13.7",
> "vary": "Accept, Cookie",
> "allow": "GET, PUT, PATCH, DELETE, HEAD, OPTIONS",
> "x-frame-options": "DENY",
> "content-length": "0",
> "x-content-type-options": "nosniff",
> "referrer-policy": "same-origin",
> "cross-origin-opener-policy": "same-origin"
> },
> "data": "",
> "url": "http://localhost/animals/6e01cf59-c8c2-430d-8fcb-a5ee6515aaea/",
> "responseTime": 47
> },
> "error": null,
> "status": "pass",
> "assertionResults": [],
> "testResults": [],
> "preRequestTestResults": [],
> "postResponseTestResults": [],
> "shouldStopRunnerExecution": false,
> "runDuration": 0.048056125,
> "name": "04_animalsの削除",
> "path": "04_animalsの削除",
> "iterationIndex": 0
> },
> {
> "test": {
> "filename": "05_animalsの許可されていないメソッド.bru"
> },
> "request": {
> "method": "HEAD",
> "url": "http://localhost:8000/animals/",
> "headers": {
> "content-type": "application/json",
> "Authorization": "Basic ********"
> }
> },
> "response": {
> "status": 200,
> "statusText": "OK",
> "headers": {
> "date": "Sun, 23 Nov 2025 02:20:11 GMT",
> "server": "WSGIServer/0.2 CPython/3.13.7",
> "content-type": "application/json",
> "vary": "Accept, Cookie",
> "allow": "GET, POST, HEAD, OPTIONS",
> "x-frame-options": "DENY",
> "x-content-type-options": "nosniff",
> "referrer-policy": "same-origin",
> "cross-origin-opener-policy": "same-origin"
> },
> "data": "",
> "url": "http://localhost/animals/",
> "responseTime": 46
> },
> "error": null,
> "status": "pass",
> "assertionResults": [],
> "testResults": [],
> "preRequestTestResults": [],
> "postResponseTestResults": [],
> "shouldStopRunnerExecution": false,
> "runDuration": 0.046924416,
> "name": "05_animalsの許可されていないメソッド",
> "path": "05_animalsの許可されていないメソッド",
> "iterationIndex": 0
> },
> {
> "test": {
> "filename": "06_animalsの存在しない詳細.bru"
> },
> "request": {
> "method": "GET",
> "url": "http://localhost:8000/animals/hoge/",
> "headers": {
> "content-type": "application/json",
> "Authorization": "Basic ********"
> }
> },
> "response": {
> "status": 404,
> "statusText": "Not Found",
> "headers": {
> "date": "Sun, 23 Nov 2025 02:20:11 GMT",
> "server": "WSGIServer/0.2 CPython/3.13.7",
> "content-type": "application/json",
> "vary": "Accept, Cookie",
> "allow": "GET, PUT, PATCH, DELETE, HEAD, OPTIONS",
> "x-frame-options": "DENY",
> "content-length": "23",
> "x-content-type-options": "nosniff",
> "referrer-policy": "same-origin",
> "cross-origin-opener-policy": "same-origin"
> },
> "data": {
> "detail": "Not found."
> },
> "url": "http://localhost/animals/hoge/",
> "responseTime": 47
> },
> "error": null,
> "status": "pass",
> "assertionResults": [],
> "testResults": [],
> "preRequestTestResults": [],
> "postResponseTestResults": [],
> "shouldStopRunnerExecution": false,
> "runDuration": 0.047566,
> "name": "06_animalsの存在しない詳細",
> "path": "06_animalsの存在しない詳細",
> "iterationIndex": 0
> },
> {
> "test": {
> "filename": "11_usersの取得.bru"
> },
> "request": {
> "method": "GET",
> "url": "http://localhost:8000/users/",
> "headers": {
> "Authorization": "Basic ********"
> }
> },
> "response": {
> "status": 200,
> "statusText": "OK",
> "headers": {
> "date": "Sun, 23 Nov 2025 02:20:11 GMT",
> "server": "WSGIServer/0.2 CPython/3.13.7",
> "content-type": "application/json",
> "vary": "Accept, Cookie",
> "allow": "GET, HEAD, OPTIONS",
> "x-frame-options": "DENY",
> "content-length": "52",
> "x-content-type-options": "nosniff",
> "referrer-policy": "same-origin",
> "cross-origin-opener-policy": "same-origin"
> },
> "data": [
> {
> "id": 1,
> "username": "ネオちゃん",
> "animals": []
> }
> ],
> "url": "http://localhost/users/",
> "responseTime": 47
> },
> "error": null,
> "status": "pass",
> "assertionResults": [],
> "testResults": [],
> "preRequestTestResults": [],
> "postResponseTestResults": [],
> "shouldStopRunnerExecution": false,
> "runDuration": 0.047630125,
> "name": "11_usersの取得",
> "path": "11_usersの取得",
> "iterationIndex": 0
> }
> ],
> "summary": {
> "totalRequests": 7,
> "passedRequests": 6,
> "failedRequests": 1,
> "errorRequests": 0,
> "skippedRequests": 0,
> "totalAssertions": 0,
> "passedAssertions": 0,
> "failedAssertions": 0,
> "totalTests": 1,
> "passedTests": 0,
> "failedTests": 1,
> "totalPreRequestTests": 0,
> "passedPreRequestTests": 0,
> "failedPreRequestTests": 0,
> "totalPostResponseTests": 0,
> "passedPostResponseTests": 0,
> "failedPostResponseTests": 0
> }
> }
> ]
> ```
[[Codex CLI]]につくってもらった。
`src/types/report.json`
```ts
/**
* bru run の JSON レポート構造。`report.json` から推測した型。
*/
export type BrunoReport = IterationReport[];
export interface IterationReport {
iterationIndex: number;
results: RequestResult[];
summary: Summary;
}
export interface Summary {
totalRequests: number;
passedRequests: number;
failedRequests: number;
errorRequests: number;
skippedRequests: number;
totalAssertions: number;
passedAssertions: number;
failedAssertions: number;
totalTests: number;
passedTests: number;
failedTests: number;
totalPreRequestTests: number;
passedPreRequestTests: number;
failedPreRequestTests: number;
totalPostResponseTests: number;
passedPostResponseTests: number;
failedPostResponseTests: number;
}
export interface RequestResult {
test: TestFile;
request: RequestInfo;
response: ResponseInfo;
error: string | null;
status: ResultStatus;
assertionResults: AssertionResult[];
testResults: AssertionResult[];
preRequestTestResults: AssertionResult[];
postResponseTestResults: AssertionResult[];
shouldStopRunnerExecution: boolean;
runDuration: number;
name: string;
path: string;
iterationIndex: number;
}
export interface TestFile {
filename: string;
}
export interface RequestInfo {
method: HttpMethod;
url: string;
headers: HeaderMap;
data?: RequestBody;
}
export interface ResponseInfo {
status: number;
statusText: string;
headers: HeaderMap;
data: ResponseBody;
url: string;
responseTime: number;
}
export interface AssertionResult {
description: string;
status: "pass" | "fail" | string;
error?: string;
actual?: unknown;
expected?: unknown;
uid?: string;
}
export type HeaderMap = Record<string, string>;
export type HttpMethod =
| "GET"
| "POST"
| "PUT"
| "PATCH"
| "DELETE"
| "HEAD"
| "OPTIONS"
| string;
export type RequestBody = string | Record<string, unknown> | unknown[] | null;
export type ResponseBody = string | Record<string, unknown> | unknown[];
// テスト結果のステータス。今後の拡張を考慮し、未知の文字列も許容する。
export type ResultStatus = "pass" | "fail" | "error" | "skipped" | string;
```
## プロダクション版で動かせるようにする
以下のコマンドで実行できる状態にする。
```console
bun serve report.json
```
[[Bun]]の型定義ファイルを追加。
```console
bun add -D @types/bun
```
`serve-cli.ts` を作成する。
`serve-cli.ts`
```ts
import Bun, { serve } from "bun";
import App from "./dist/index.html";
const [, , reportPath, port = 3000] = Bun.argv;
if (!reportPath) {
console.error("Usage: bun serve <report.json>");
process.exit(1);
}
const reportData = JSON.parse(await Bun.file(reportPath).text());
serve({
port,
routes: {
"/": App,
"/api/report": {
GET: async () => Response.json(reportData),
},
},
});
console.log(`Serving report at http://localhost:${port}`);
console.log("Press Ctrl+C to stop.");
```
実行権限もつけておく。
```console
chmod +x serve-cli.ts
```
`vite.config.ts` も修正。
`vite.config.ts`
```ts
export default defineConfig({
plugins: [vue(), tailwindcss()],
// ★追加
base: "./",
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});
```
`package.json` の `scripts` に `serve` と `build:serve` を追加。
```json
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"serve": "bun serve-cli",
"build:serve": "bun run build && bun serve",
"preview": "vite preview"
},
```
`dist` 配下に[[Vue]]の成果物を配置する。
```console
bun run build
```
```
dist
├── assets
│ ├── index-BfK9--IS.js
│ └── index-BVqwEFAo.css
└── index.html
```
起動コマンドを実行してエラーが出ずに画面が表示されればOK。
```console
bun serve report.json
```
> [!error] serve-cli.tsで起動したあとに http://localhost:3000/ にアクセスするとエラーになる場合
>
> ```
> dist/index.html
> error: Could not resolve: "/assets/index-BfK9--IS.js"
> error: Could not resolve: "/assets/index-BVqwEFAo.css"
> Errors during a build can only be dismissed by fixing them.
> ```
>
> `vite.config.ts` で `base` に `./` を指定してあるか確認する。その設定が入っていると `./assets/...` になるが、入っていないと `/assets/...` となり見つからない。
## `report.json` の情報を読み込む
`http://localhost:3000/report` で `report.json` が読み込めるようになっているので、これを[[Vue]]から呼び出す。
`app.vue`
```html
<script setup lang="ts">
import { ref } from "vue";
import { Button } from "./components/ui/button";
const report = ref<object | null>(null);
const fetchReport = async () => {
try {
const response = await fetch("/api/report");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
report.value = await response.json();
console.log("Report fetched:", report.value);
} catch (error) {
console.error("Failed to fetch report.json:", error);
}
};
</script>
<template>
<Button @click="fetchReport">Fetch report.json</Button>
<pre v-if="report">{{ JSON.stringify(report, null, 2) }}</pre>
</template>
```
これで上手くいったかと思いきや、開発時は裏でサーバーを立ち上げなければいけないので、`bun serve report.json` と `bun dev` が必要である。しかもポート番号が異なるので地味にめんどい。[[CORS]]も発生する。
[[CORS]]は[[プロキシを設定 (Vite)|プロキシを設定]]で回避できた。
`vite.config.ts`
```ts
import tailwindcss from "@tailwindcss/vite";
import vue from "@vitejs/plugin-vue";
import path from "node:path";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [vue(), tailwindcss()],
base: "./",
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
server: {
proxy: {
"/api": {
target: "http://localhost:3000",
changeOrigin: true,
},
},
},
});
```
## 動作確認に必要なコマンド
### 開発時
- **Web:** `bun dev` (hot reload)
- `localhost:5173` にアクセス
- **API**: `bun serve <report.json>`
### 利用時
- `bun serve <report.json>`
- `localhost:3000 にアクセス`
## 機能をつくっていく
- [x] report.json をもう少し具体的にする
- [x] ヘッダ
- [x] 領域確保
- [x] サイドコンテンツ
- [x] 領域確保
- [x] 可変width
- [x] スクロール
- [x] ヘッダ
- [x] 全体の概要を表示
- [x] passedRequests
- [x] failedRequests
- [x] skippedRequests
- [x] styling
- [x] リスト
- [x] 結果の一覧を表示
- [x] styling
- [x] ステータスフィルター ON/OFF
- [x] メインコンテンツ
- [x] 領域確保
- [x] 可変width
- [x] スクロール
- [x] 表示アニメーション
- [x] テスト結果タブ
- [x] 領域確保
- [x] 表示
- [x] 失敗したパスの表示
- [x] 成功したパスの表示
- [x] styling
- [x] レスポンスタブ
- [x] 領域確保
- [x] 表示
- [x] レスポンス
- [x] レスポンスのハイライト
- [x] レスポンスヘッダ
- [x] styling
- [x] レスポンスヘッダ
- [x] レスポンス
- [x] コンポーネント化
- [x] ステータスバッジ
- [x] CodeBlockにコピーボタン
- [x] リクエストタブ
- [x] 領域確保
- [x] 表示
- [x] HTTPメソッド
- [x] URL
- [x] リクエストヘッダ
- [x] リクエストボディ
- [x] styling
- [x] コンポーネント化
- [x] メソッドバッジ
## `serve-cli` の変更
`public/` 配下のリソースを配信できないことに気づいたので、`index.html` などの配信方法を変更。 `/api/report` 以外のものを静的サーバーとして扱い、`dist/` 配下の配信をすることに。
`serve-cli.ts`
```ts
import Bun, { serve } from "bun";
import path from "node:path";
const [, , reportPath, port = 3000] = Bun.argv;
if (!reportPath) {
console.error("Usage: bun serve <report.json>");
process.exit(1);
}
const reportData = JSON.parse(await Bun.file(reportPath).text());
const distDir = path.join(import.meta.dir, "dist");
const indexHtmlPath = path.join(distDir, "index.html");
serve({
port,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/api/report") {
return Response.json(reportData);
}
// Map / -> /index.html, otherwise serve the file from dist
const filePath = url.pathname === "/"
? indexHtmlPath
: path.join(distDir, url.pathname);
const file = Bun.file(filePath);
if (await file.exists()) {
return new Response(file);
}
return new Response("Not Found", { status: 404 });
},
});
console.log(`Serving report at http://localhost:${port}`);
console.log("Press Ctrl+C to stop.");
```
## リポジトリの作成
<div class="link-card-v2">
<div class="link-card-v2-site">
<img class="link-card-v2-site-icon" src="https://github.githubassets.com/favicons/favicon.svg" />
<span class="link-card-v2-site-name">GitHub</span>
</div>
<div class="link-card-v2-title">
GitHub - tadashi-aikawa/bruno-report-viewer: A lightweight browser-based viewer for quickly inspecting Bruno report JSON.
</div>
<div class="link-card-v2-content">
A lightweight browser-based viewer for quickly inspecting Bruno report JSON. - tadashi-aikawa/bruno-report-viewe ...
</div>
<img class="link-card-v2-image" src="https://opengraph.githubassets.com/a456c5d26f703e23d0121d7521836f60f80fb5ed0250079ae06395839f8fc472/tadashi-aikawa/bruno-report-viewer" />
<a href="https://github.com/tadashi-aikawa/bruno-report-viewer?tab=readme-ov-file"></a>
</div>