Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,14 @@ always asks before it touches your local dashboard.

## Widgets

33 tools across 10 categories, and growing.
36 tools across 10 categories, and growing.

| Category | Widgets |
| ---------- | --------------------------------------------------------------------------------------------------------- |
| Generators | UUID Generator |
| Formatting | Cron Expression, JSON Formatter, Timestamp Converter, XML Formatter, YAML ↔ JSON Converter |
| Encoding | Base64, JWT Encoder, LZ-String, URL Encoder |
| Security | Certificate Viewer, Hash Generator, JWK Viewer, Password Generator |
| Encoding | Base64, JWT Encoder, LZ-String, Protobuf Decoder, URL Encoder |
| Security | Certificate Viewer, Hash Generator, JWK Viewer, Password Generator, Unix Permissions |
| Text | Content Type Detector, Emoji Picker, Log Viewer, Notes, Regex Tester, Text Case Converter, Text Diff |
| Math | Expression Evaluator, Number Base Converter, Percentage Calculator, Statistics Calculator, Unit Converter |
| AI / LLM | Invisible Character Cleaner, Token Counter |
Expand Down
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"lucide-react": "^1.30.0",
"lz-string": "^1.5.0",
"nanoid": "^6.0.1",
"protobufjs": "^8.8.0",
"qrcode.react": "^4.2.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
Expand Down
165 changes: 165 additions & 0 deletions src/widgets/protobuf-decoder/ProtobufDecoderWidget.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { describe, expect, it } from 'vitest'
import { render, screen, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { setCodeMirrorValue } from '@/test/codemirror'
import ProtobufDecoderWidget from './ProtobufDecoderWidget'

/** The sample payload the widget opens on: Person { id: 150, name:
* "testing", tags: ["a", "b"], home: { city: "Paris" } }. */
const SAMPLE_PAYLOAD = 'CJYBEgd0ZXN0aW5nGgFhGgFiIgcKBVBhcmlz'

function payloadBox() {
return screen.getByPlaceholderText(/paste a base64 or hex protobuf payload/i)
}

async function setPayload(user: ReturnType<typeof userEvent.setup>, text: string) {
await user.clear(payloadBox())
if (text) await user.paste(text)
}

describe('ProtobufDecoderWidget', () => {
it('decodes the sample payload without a schema, field by field', () => {
render(<ProtobufDecoderWidget instanceId="test" mode="grid" />)

const rows = screen.getAllByRole('listitem')
expect(within(rows[0]).getByText('1')).toBeInTheDocument()
expect(within(rows[0]).getByText('150')).toBeInTheDocument()
expect(within(rows[0]).getByText('varint')).toBeInTheDocument()
expect(screen.getByText('"testing"')).toBeInTheDocument()
expect(screen.getByText('"Paris"')).toBeInTheDocument()
})

it('says what the encoding and size turned out to be', () => {
render(<ProtobufDecoderWidget instanceId="test" mode="grid" />)
expect(screen.getByText(/base64, 27 bytes/i)).toBeInTheDocument()
})

it('reads the same payload pasted as hex', async () => {
const user = userEvent.setup()
render(<ProtobufDecoderWidget instanceId="test" mode="grid" />)

await setPayload(user, '08 96 01 12 07 74 65 73 74 69 6e 67')

expect(screen.getByText(/hex, 12 bytes/i)).toBeInTheDocument()
expect(screen.getByText('"testing"')).toBeInTheDocument()
})

it('strips a gRPC frame and says it did', async () => {
const user = userEvent.setup()
render(<ProtobufDecoderWidget instanceId="test" mode="grid" />)

await setPayload(user, '0000000003089601')

expect(screen.getByText(/gRPC frame detected/i)).toBeInTheDocument()
expect(screen.getByText('150')).toBeInTheDocument()
})

it('offers the other readings of an ambiguous value', async () => {
const user = userEvent.setup()
render(<ProtobufDecoderWidget instanceId="test" mode="grid" />)

// Field 1, varint 1: as likely a bool as a number.
await setPayload(user, '0801')

expect(screen.getByText(/or bool true/i)).toBeInTheDocument()
})

it('explains what a schema-less decode cannot know', () => {
render(<ProtobufDecoderWidget instanceId="test" mode="grid" />)
expect(screen.getByText(/names and declared types are not in the bytes/i)).toBeInTheDocument()
})

it('names the fields once the .proto is supplied', async () => {
const user = userEvent.setup()
render(<ProtobufDecoderWidget instanceId="test" mode="grid" />)

await user.click(screen.getByRole('button', { name: /with \.proto/i }))

expect(screen.getByRole('combobox', { name: /message/i })).toHaveValue('Person')
expect(screen.getByText('name')).toBeInTheDocument()
expect(screen.getByText('"testing"')).toBeInTheDocument()
expect(screen.getByText('city')).toBeInTheDocument()
})

it('lets the message type be switched to another one in the definition', async () => {
const user = userEvent.setup()
render(<ProtobufDecoderWidget instanceId="test" mode="grid" />)

await user.click(screen.getByRole('button', { name: /with \.proto/i }))
await user.selectOptions(screen.getByRole('combobox', { name: /message/i }), 'Address')

// Person's payload is not an Address. protobufjs decodes it to an empty
// object without complaining, so the widget is what has to say why.
expect(screen.getByText(/Field 1 arrived as varint but is declared string city/i)).toBeInTheDocument()
expect(screen.getByText(/Fields 2, 3, 4 on the wire/i)).toBeInTheDocument()
})

it('reports a syntax error in the definition with its line', async () => {
const user = userEvent.setup()
render(<ProtobufDecoderWidget instanceId="test" mode="grid" />)

await user.click(screen.getByRole('button', { name: /with \.proto/i }))
setCodeMirrorValue(
screen.getByRole('textbox', { name: 'Proto definition' }),
'syntax = "proto3";\nmessage B { int32 id = ; }',
)

expect(screen.getByText(/line 2/i)).toBeInTheDocument()
})

it('warns when the payload carries fields the definition never declared', async () => {
const user = userEvent.setup()
render(<ProtobufDecoderWidget instanceId="test" mode="grid" />)

await user.click(screen.getByRole('button', { name: /with \.proto/i }))
// Field 1 (declared) plus field 9 (not in Person).
await setPayload(user, '0896014a0568656c6c6f')

expect(screen.getByText(/Field 9 on the wire is not in this definition/i)).toBeInTheDocument()
})

it('reports an unusable payload instead of decoding noise', async () => {
const user = userEvent.setup()
render(<ProtobufDecoderWidget instanceId="test" mode="grid" />)

await setPayload(user, 'not a payload!!')

expect(screen.getByText(/neither valid hex nor valid base64/i)).toBeInTheDocument()
})

it('keeps what it read before a truncated payload gave out', async () => {
const user = userEvent.setup()
render(<ProtobufDecoderWidget instanceId="test" mode="grid" />)

await setPayload(user, '0896011209746573')

expect(screen.getByText('150')).toBeInTheDocument()
expect(screen.getByText(/past the end of the payload/i)).toBeInTheDocument()
expect(screen.getByText(/everything above was read before that point/i)).toBeInTheDocument()
})

it('keeps payload, mode and definition across a remount of the same instance', async () => {
const user = userEvent.setup()
const { unmount } = render(<ProtobufDecoderWidget instanceId="test" mode="grid" />)

await user.click(screen.getByRole('button', { name: /with \.proto/i }))
await setPayload(user, '089601')
unmount()

render(<ProtobufDecoderWidget instanceId="test" mode="grid" />)
expect(screen.getByRole('button', { name: /with \.proto/i })).toHaveAttribute('aria-pressed', 'true')
expect(payloadBox()).toHaveValue('089601')
expect(screen.getByText('id')).toBeInTheDocument()
})

it('still decodes the sample payload after it has been re-pasted', async () => {
const user = userEvent.setup()
render(<ProtobufDecoderWidget instanceId="test" mode="grid" />)

await setPayload(user, '')
expect(screen.getByText(/paste a base64 or hex payload/i)).toBeInTheDocument()

await user.paste(SAMPLE_PAYLOAD)
expect(screen.getByText('"testing"')).toBeInTheDocument()
})
})
Loading
Loading