Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e14a1ea
ci: run the built Worker and request every page (#186)
anurag629 Oct 17, 2026
689a201
chore(deps): update next and @opennextjs/cloudflare in one Dependabot…
anurag629 Oct 17, 2026
1ed745a
feat(seo): add inLanguage, isAccessibleForFree and site dates to json…
prakashNalla Oct 17, 2026
b3d036d
feat(color-converter): add color converter & contrast checker (#84) (…
bhushan4work Oct 17, 2026
4a719fa
fix(history): give the history panel the tool's useHistory instance (…
tnalbant Oct 18, 2026
2060a24
ci: fail on a BOM or text garbled by the wrong encoding (#194)
anurag629 Oct 19, 2026
bba09c5
ci: skip the smoke test checks when Cloudflare challenges the runner …
anurag629 Oct 19, 2026
d71777f
docs: say the PR has to be opened here, not inside a fork (#197)
anurag629 Oct 19, 2026
968bd8a
feat: add Unix timestamp converter (#190)
piyusshhjangid Oct 19, 2026
40d1372
chore(deps-dev): bump the dev-dependencies group across 1 directory w…
dependabot[bot] Oct 19, 2026
3cbcb91
Fix: Update parseSyntaxError to use lossless-json for accurate error …
sameerakhtar406 Oct 19, 2026
c5af43f
fix: keep command palette selection visible during keyboard navigatio…
0x5t4l1n Oct 19, 2026
53df087
feat: add Text Case Converter tool (#199)
harshit-exe-dev Oct 19, 2026
7c72124
fix(storage): don't write the initial value over stored data on mount…
prakashNalla Oct 19, 2026
fdbb758
fix(sitemap): derive per-url lastModified from build metadata (#46) (…
bhushan4work Oct 19, 2026
cf96694
fix: give the JWT decoder switch real tab semantics and name its fiel…
varun-pahuja Oct 20, 2026
27b3410
FIxed hydration error (#208)
anurag2787 Oct 20, 2026
6e9ca65
ci: add hacktoberfest-accepted to merged PRs when it was missed (#203)
anurag629 Oct 20, 2026
8360709
fix(base64): improve mime detection for avif, svg, bmp, and json (#20…
bhushan4work Oct 20, 2026
87b37b8
fix(invoice): round each row the same way as the subtotal (#212)
sameerakhtar406 Oct 20, 2026
224c52d
fix(json): handle empty and nested arrays in XML conversion (#211)
anurag2787 Oct 20, 2026
5a3b330
fix: quote YAML 1.1 ambiguous strings (#202)
Vasundhara117 Oct 20, 2026
cddd5ff
fix(invoice): stop the page jumping while the invoice loads (#213)
Saifjafri212213 Oct 20, 2026
5eacb44
fix: keep JSON Find focus and highlight full matches (#209)
kudala-bharani Oct 21, 2026
03761f6
fix(json): reject duplicate keys instead of crashing the tool (#222)
lb1192176991-lab Oct 21, 2026
195911c
ci: add Lighthouse CI performance budgets (#48) (#215)
bhushan4work Oct 21, 2026
f8600dd
test: set up component tests with happy-dom and Testing Library (#227)
Bl1ckxy Oct 21, 2026
2c49963
chore(deps): bump react, react-dom, lossless-json and wrangler (#228)
anurag629 Oct 21, 2026
5eda72b
fix(invoice): keep the prefix and padding of the last invoice number …
Saifjafri212213 Oct 21, 2026
76787ac
fix(base64): don't switch to Decode when plain text is pasted (#231)
Saifjafri212213 Oct 21, 2026
9df7fe5
fix(case-converter): drop punctuation from camelCase, snake_case and …
prakashNalla Oct 21, 2026
26483c1
preserve invalid XML tag names (#239)
anurag2787 Oct 21, 2026
69e4613
fix(jwt-decoder): treat inherited property names as ordinary claims (…
dbdefault Oct 21, 2026
e8fc92f
fix(json): write YAML numbers with an exponent as floats PyYAML can r…
Saifjafri212213 Oct 21, 2026
434166e
fix(url-encoder): make the encode/decode tabs work with the keyboard …
prakashNalla Oct 21, 2026
04f078f
fix(color-converter): truncate the shown contrast ratio instead of ro…
bercedev Oct 22, 2026
f1c3f96
feat: add Number Base Converter tool (#244)
aryanjsx Oct 22, 2026
1fc1f33
fix(timestamp-converter): reject impossible calendar days in toEpoch …
dbdefault Oct 22, 2026
9f450d0
fix(json-formatter): ignore inherited CSV cell values (#257)
Pratham2511 Oct 22, 2026
f705fdb
fix: announce the selected option in six two-option switches (#258)
aryanjsx Oct 22, 2026
64f31a1
fix(base64): detect UTF-8 text with multibyte characters as plain tex…
rahul05ranjan Oct 22, 2026
4b655d0
fix: show the selected option of the two-option switches in the light…
aryanjsx Oct 22, 2026
5ad091d
fix: preserve input when swap fails (#261)
piyusshhjangid Oct 22, 2026
17ef1bc
fix(invoice-generator): preserve negative and decimal input (#263)
akshatXD-hash Oct 22, 2026
062e632
fix(base64): decode data URIs that have parameters before ;base64 (#264)
Saifjafri212213 Oct 22, 2026
32425cf
fix(json): keep every digit of numbers in JSONPath matches (#265)
arxh80 Oct 22, 2026
b52d8be
fix: add accessible labels to JWT controls (#267)
Pranitha06sri Oct 22, 2026
0183541
fix(invoice): restore row font before measuring description (#246) (#…
bhushan4work Oct 22, 2026
7bd3fc1
feat(guides): add typed guides registry and routing (#35) (#269)
bhushan4work Oct 23, 2026
f1ab01b
chore: version 0.8.0 (#271)
anurag629 Oct 23, 2026
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
4 changes: 4 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ body:
- Base64
- URL encoder/decoder
- Invoice generator
- Color converter & contrast checker
- Unix timestamp converter
- Text case converter
- Number base converter
- Shell (theme, command palette, header)
- Other / not sure
validations:
Expand Down
15 changes: 15 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ updates:
interval: weekly
open-pull-requests-limit: 5
groups:
# Next and the OpenNext adapter have to move together. next 16.4.0 on its own
# (#169) broke every page until @opennextjs/cloudflare 1.20.12 (v0.7.0).
# Dependabot puts a dependency in the first group it matches, so this one
# has to stay above dev-dependencies to take eslint-config-next.
next:
patterns:
- "next"
- "@opennextjs/cloudflare"
- "eslint-config-next"
# react-dom refuses to load next to a react of a different version, so a
# bump of one without the other fails the tests (#224 and #226).
react:
patterns:
- "react"
- "react-dom"
dev-dependencies:
dependency-type: development
ignore:
Expand Down
89 changes: 89 additions & 0 deletions .github/workflows/accepted-label.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: Accepted label

# A merged PR counts on the leaderboard only with the hacktoberfest-accepted
# label, and only for an issue that was assigned to its author (CONTRIBUTING.md).
# Maintainers add the label when they merge. This adds it when that was missed,
# by the same rule as the claim check: every issue the PR closes is assigned to
# the author. It runs once, at the merge, so removing the label later sticks.
#
# It uses pull_request_target so it can label fork PRs. That is safe here: the
# job never checks out or runs the PR's code.
on:
pull_request_target:
branches: [develop]
types: [closed]

permissions:
issues: read
pull-requests: write

jobs:
label:
name: Label a merged contributor PR
# Maintainers and bots don't claim issues, and spam never counts.
if: >-
github.event.pull_request.merged == true &&
github.event.pull_request.user.type != 'Bot' &&
!contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association) &&
!contains(github.event.pull_request.labels.*.name, 'hacktoberfest-accepted') &&
!contains(github.event.pull_request.labels.*.name, 'spam')
runs-on: ubuntu-latest
steps:
- name: Add the label if every linked issue is assigned to the author
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail

pr=$(gh api graphql \
-f owner="${REPO%/*}" -f name="${REPO#*/}" -F pr="$PR" \
-f query='query($owner: String!, $name: String!, $pr: Int!) {
repository(owner: $owner, name: $name) {
pullRequest(number: $pr) {
author { login }
body
closingIssuesReferences(first: 20) {
nodes { number repository { nameWithOwner } assignees(first: 20) { nodes { login } } }
}
}
}
}' \
--jq '.data.repository.pullRequest')
author=$(jq -r '.author.login // ""' <<<"$pr")

# The issues this PR closed ("Closes #12" in the description).
linked=$(jq --arg repo "$REPO" '[ .closingIssuesReferences.nodes[] | {
ref: (if .repository.nameWithOwner == $repo then "#\(.number)" else "\(.repository.nameWithOwner)#\(.number)" end),
assignees: [.assignees.nodes[].login]
} ]' <<<"$pr")

# Same fallback as the claim check: read "Closes #12" from the description.
if [ "$(jq length <<<"$linked")" -eq 0 ]; then
numbers=$(jq -r '[ (.body // "") | scan("(?i)\\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s*:?\\s+#([0-9]+)") | .[0] ] | unique | .[]' <<<"$pr")
for number in $numbers; do
if issue=$(gh api "repos/$REPO/issues/$number" --jq 'select(.pull_request == null) | {ref: "#\(.number)", assignees: [.assignees[].login]}' 2>/dev/null) && [ -n "$issue" ]; then
linked=$(jq --argjson issue "$issue" '. + [$issue]' <<<"$linked")
fi
done
fi

if [ "$(jq length <<<"$linked")" -eq 0 ]; then
echo "::notice title=Not labeled::This PR isn't linked to an issue, so it doesn't get hacktoberfest-accepted."
exit 0
fi

missing=$(jq -r --arg author "$author" '
[ .[]
| select(all(.assignees[]; ascii_downcase != ($author | ascii_downcase)))
| .ref ]
| join(", ")' <<<"$linked")

if [ -n "$missing" ]; then
echo "::notice title=Not labeled::$missing isn't assigned to @$author, so this PR doesn't get hacktoberfest-accepted."
exit 0
fi

gh api -X POST "repos/$REPO/issues/$PR/labels" -f "labels[]=hacktoberfest-accepted" --silent
echo "Added hacktoberfest-accepted: $(jq -r '[.[].ref] | join(", ")' <<<"$linked") assigned to @$author"
69 changes: 68 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,78 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
# Some editors save files with a UTF-8 BOM, and a tool that reads UTF-8 with
# the wrong encoding turns a dash or an arrow into two or three other
# characters. Both have reached PRs here, so catch them before review.
- name: No BOM or garbled characters
run: |
if git grep -n -I -F -e $'\xEF\xBB\xBF' -e $'\xCE\x93\xC3\x87' -e $'\xCE\x93\xC3\xA5\xC3\x86' \
-e $'\xE2\x94\xAC\xE2\x95\x96' -e $'\xC3\xA2\xE2\x82\xAC' -e $'\xC3\x83\xC2\xA9'; then
echo "::error::The lines above start with a UTF-8 BOM or have text garbled by the wrong encoding. Save the file as UTF-8 without a BOM, and copy garbled lines back from the base branch."
exit 1
fi
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run test
- run: npm run build
# Builds the Worker the same way Workers Builds does. This runs `npm run build`
# (next build) first, so there is no separate build step.
- run: npx opennextjs-cloudflare build
# A green next build doesn't mean the Worker runs. v0.7.0 built fine and then
# threw on every request, so start the built Worker in local workerd and
# request every page in the sitemap, the og:image each page links to and a
# missing page.
- name: Request every page from the built Worker
env:
WRANGLER_SEND_METRICS: "false"
run: |
npx wrangler dev --ip 127.0.0.1 --port 8787 > wrangler.log 2>&1 &
base=http://127.0.0.1:8787
for i in $(seq 90); do
curl -s -o /dev/null "$base/robots.txt" && break
sleep 1
done
pages=$(curl -s "$base/sitemap.xml" | grep -o '<loc>[^<]*' | sed -E 's|<loc>https?://[^/]+||')
fail=0
if [ -z "$pages" ]; then
echo "sitemap.xml has no pages"
fail=1
fi
check() {
code=$(curl -s -o body.out -w '%{http_code}' "$base$1" || true)
echo "$code $1"
if [ "$code" != "$2" ]; then fail=1; fi
}
for p in $pages; do
check "$p" 200
img=$(grep -o 'property="og:image" content="[^"]*' body.out | head -1 | sed -E 's|.*content="https?://[^/]+||; s|&amp;|\&|g')
if [ -n "$img" ]; then
check "$img" 200
else
echo "no og:image on $p"
fail=1
fi
done
check /sitemap.xml 200
check /robots.txt 200
check /no-such-page 404
if grep -q 'wrangler:error' wrangler.log; then fail=1; fi
if [ "$fail" != 0 ]; then
echo "The built Worker failed. Wrangler output:"
cat wrangler.log
exit 1
fi
- name: Lighthouse CI performance budgets
run: |
if ! curl -s -o /dev/null http://127.0.0.1:8787/robots.txt; then
npx wrangler dev --ip 127.0.0.1 --port 8787 > wrangler.log 2>&1 &
for i in $(seq 30); do
curl -s -o /dev/null http://127.0.0.1:8787/robots.txt && break
sleep 1
done
fi
npm run lighthouse
pkill -f "wrangler dev" || true
20 changes: 18 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,25 @@ jobs:
needs: release
runs-on: ubuntu-latest
env:
# The repo variable points at the workers.dev URL until the
# tools.codercops.com cutover, and at the real host after it.
# Set the SMOKE_BASE_URL repo variable to check another host.
BASE_URL: ${{ vars.SMOKE_BASE_URL || 'https://tools.codercops.com' }}
EXPECTED_SHA: ${{ github.event.pull_request.merge_commit_sha }}
steps:
# Bot Fight Mode on codercops.com answers GitHub's runners with a
# Cloudflare challenge (403 with cf-mitigated: challenge) instead of the
# site, and production has no workers.dev address to check instead. Then
# the checks below can't see the deploy, so they're skipped with a notice
# and the live site gets checked by hand.
- name: Check whether Cloudflare challenges this runner
run: |
read -r STATUS MITIGATED < <(curl -s -o /dev/null -w "%{http_code} %header{cf-mitigated}\n" --max-time 15 "$BASE_URL/version.txt")
if [ "$STATUS" = "403" ] && [ "$MITIGATED" = "challenge" ]; then
echo "::notice::Cloudflare challenged this runner on $BASE_URL, so the deploy wasn't checked from here. Check the live site by hand."
echo "CHALLENGED=1" >> "$GITHUB_ENV"
fi

- name: Wait for the Cloudflare deploy of this merge
if: env.CHALLENGED != '1'
run: |
# Workers Builds deploys production on its own. version.txt carries
# the commit it built, so poll until it matches (up to 15 minutes).
Expand All @@ -95,6 +108,7 @@ jobs:
exit 1

- name: Homepage reachable
if: env.CHALLENGED != '1'
run: |
STATUS=$(curl -sL -o /dev/null -w "%{http_code}" --max-time 15 "$BASE_URL/")
if [ "$STATUS" != "200" ]; then
Expand All @@ -104,6 +118,7 @@ jobs:
echo "Homepage check passed"

- name: Tool page reachable
if: env.CHALLENGED != '1'
run: |
STATUS=$(curl -sL -o /dev/null -w "%{http_code}" --max-time 15 "$BASE_URL/json-formatter")
if [ "$STATUS" != "200" ]; then
Expand All @@ -113,6 +128,7 @@ jobs:
echo "Tool page check passed"

- name: Sitemap valid
if: env.CHALLENGED != '1'
run: |
STATUS=$(curl -sL -o /dev/null -w "%{http_code}" --max-time 15 "$BASE_URL/sitemap.xml")
if [ "$STATUS" != "200" ]; then
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,6 @@ generated/
# local tooling notes
CLAUDE.md
AGENTS.md

# lighthouse ci reports
.lighthouseci/
7 changes: 5 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ We're taking part in Hacktoberfest this year, and first-time contributors are ve
2. Comment on it to ask for it. I'll assign it to you, usually within a day. Please don't start on an issue that's assigned to someone else.
3. Take one issue at a time. Once your PR is merged, grab the next one. After 3 merged PRs with us you can hold two issues at once.
4. If you're assigned and there's no PR or update for 5 days, I'll free the issue up for someone else. Just comment if you need more time.
5. Open your PR against `develop` and put `Closes #<issue number>` in the description. A check on the PR ("Claim check") fails if that issue isn't assigned to you. Another one ("Description check") fails while the "How I tested this" or "AI assistance" section of the description is missing or empty.
5. Open your PR against `develop` here in codercops/toolbelt and put `Closes #<issue number>` in the description. A PR merged inside your own fork never reaches us, so it doesn't count. A check on the PR ("Claim check") fails if that issue isn't assigned to you. Another one ("Description check") fails while the "How I tested this" or "AI assistance" section of the description is missing or empty.

PRs count for Hacktoberfest once they're merged or labeled `hacktoberfest-accepted`. PRs that aren't linked to an issue, only reformat code, or change things nobody asked for will be closed, and spammy ones get the `spam` label. If you have an idea that isn't an issue yet, open an issue first so we can agree on it before you write code. The full rules are under [Ground rules](#ground-rules) below.

Expand Down Expand Up @@ -80,6 +80,8 @@ Before opening a pull request, make sure this passes:
npm run lint && npm run test && npm run build
```

CI also builds the Cloudflare Worker and requests every page from it in local workerd. If that step fails while `npm run build` passes locally, run `npm run preview` and open the page the CI log names; the error shows up in the terminal.

## How the code is organized

- Pure logic lives in `lib/` (parsing, crypto, PDF, conversions). It has no React and no DOM, so it is easy to test. Put new logic here, not inside components.
Expand All @@ -90,6 +92,7 @@ npm run lint && npm run test && npm run build
## Guidelines

- Add or update a test in `lib/__tests__/` for any behavior change to `lib/`. The bugs this project has fixed were exactly the kind unit tests catch (number precision, money rounding, encoding edge cases).
- Add component tests in `components/` (or the corresponding `app/` route directory) using the happy-dom test project.
- Keep the change focused. Small, single-purpose PRs are reviewed faster.
- Match the surrounding style. There is no separate formatter config; follow the existing code.
- No analytics, trackers, or network calls that send user data anywhere. The privacy claim is the whole point.
Expand All @@ -114,7 +117,7 @@ toolbelt follows the standard CODERCOPS flow:

- `develop` is the default and integration branch; `production` is the deployed branch (tools.codercops.com). Never commit directly to either.
- Start every change on a branch off `develop`: `feat/*`, `fix/*`, `chore/*`, or `docs/*`.
- Open a PR into `develop` and **squash and merge** it (one squashed commit per feature). Use conventional commit titles (`feat:`, `fix:`, `chore:`, `docs:`). CI (lint, tests, build) must be green.
- Open a PR into `develop` and **squash and merge** it (one squashed commit per feature). Use conventional commit titles (`feat:`, `fix:`, `chore:`, `docs:`). CI (lint, tests, build, and the page check on the built Worker) must be green.
- A release is a PR from `develop` into `production`, merged as a **merge commit** (not squash), so production keeps full history. Bump the `package.json` version in that PR, and add a `release:minor` or `release:major` label if it is not a patch.
- Merging the release PR runs the Release workflow: it tags `vX.Y.Z`, publishes a GitHub Release, smoke-tests production, and fast-forwards `develop` back up to `production`.

Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ A developer's toolbelt: fast, privacy-first browser tools. Everything runs clien
- **[JWT decoder and inspector](https://tools.codercops.com/jwt-decoder).** Decode header, payload, and claims; verify HS/RS/PS/ES signatures via WebCrypto; security audit; HS256/384/512 encoder.
- **[Base64 encoder and decoder](https://tools.codercops.com/base64).** Text, images, and files; URL-safe variant; data URIs; magic-byte detection; Base32/58/85 and hashes.
- **[Invoice generator](https://tools.codercops.com/invoice-generator).** Live preview, vector PDF export with Unicode currency support, a reusable sender profile, all saved locally.
- **[Color converter & contrast checker](https://tools.codercops.com/color-converter).** Convert between HEX, RGB, and HSL with alpha channel support, live swatch, and WCAG AA/AAA contrast ratios for normal and large text.

## Why it exists

Expand Down Expand Up @@ -114,13 +115,23 @@ The tool set is a single typed registry in [`lib/tools.ts`](lib/tools.ts). The h
2. Create `app/(tools)/<slug>/page.tsx` (metadata plus your client component), its `<Name>Client.tsx`, and an `opengraph-image.tsx` (a few lines that pass the registry entry to the shared renderer).
3. Put pure logic in `lib/` and cover it with a test in `lib/__tests__/`.

## Add a guide

The guides collection is a single typed registry in [`lib/guides.ts`](lib/guides.ts). The guides index, per-guide metadata, JSON-LD (Article, HowTo, BreadcrumbList), social share images, and sitemap all derive from it. To add a guide:

1. Add an entry to the `GUIDES` array in `lib/guides.ts`.
2. Create `app/guides/<slug>/page.tsx` (metadata plus your guide content) and an `opengraph-image.tsx` (a few lines that pass the registry entry to the shared `renderGuideOgImage`).
3. Add a test in `lib/__tests__/guides.test.ts` to ensure metadata, routes, and structured-data invariants hold.

## Project layout

```
app/ shell (layout, error/not-found boundaries, metadata routes)
app/(tools)/ one route per tool, sharing a layout and ToolPageLayout
app/guides/ guides index and walkthroughs
components/shared/ header, theme, toasts, command palette, reusable UI
lib/ pure per-tool logic (parsing, crypto, PDF, conversions), unit-tested
lib/guides.ts the guides registry (single source of truth for guides)
lib/tools.ts the tool registry (single source of truth)
lib/jsonLd.ts schema.org builders derived from the registry
lib/og.tsx the shared Open Graph card renderer
Expand Down
47 changes: 47 additions & 0 deletions app/(tools)/base64/components/DecodeTab.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { ToastProvider } from "@/components/shared/Toast";
import { DecodeTab } from "./DecodeTab";

function decode(input: string) {
render(
<ToastProvider>
<DecodeTab />
</ToastProvider>,
);
fireEvent.change(screen.getByLabelText("Base64 to decode"), { target: { value: input } });
}

const notice = () => screen.queryByText(/Detected data URI/);
const infoValue = (label: string) => screen.getByText(label).nextElementSibling?.textContent;

describe("DecodeTab data URIs", () => {
// Vitest globals are off, so Testing Library does not clean up on its own.
afterEach(cleanup);

it.each([
["data:text/plain;base64,SGVsbG8gV29ybGQ=", "text/plain", "Hello World"],
["data:text/plain;charset=utf-8;base64,SGVsbG8gV29ybGQ=", "text/plain", "Hello World"],
["data:application/json;charset=UTF-8;base64,eyJhIjoxfQ==", "application/json", '{"a":1}'],
["data:text/plain;charset=utf-8;format=flowed;base64,SGVsbG8gV29ybGQ=", "text/plain", "Hello World"],
["DATA:text/plain;BASE64,SGVsbG8gV29ybGQ=", "text/plain", "Hello World"],
["data:;base64,SGVsbG8gV29ybGQ=", "text/plain", "Hello World"],
[" data:text/plain;charset=utf-8;base64,SGVsbG8gV29ybGQ=\n", "text/plain", "Hello World"],
])("decodes %j as %s", (uri, mime, text) => {
decode(uri);
expect(notice()).toBeInTheDocument();
expect(screen.queryByText(/illegal character/)).not.toBeInTheDocument();
expect(infoValue("MIME")).toBe(mime);
expect(screen.getByText(text)).toBeInTheDocument();
});

it("shows the whole prefix it stripped, parameters included", () => {
decode("data:text/plain;charset=utf-8;base64,SGVsbG8gV29ybGQ=");
expect(screen.getByText("data:text/plain;charset=utf-8;base64,")).toBeInTheDocument();
});

it("does not treat a data URI without ;base64 as base64", () => {
decode("data:text/plain,hello");
expect(notice()).not.toBeInTheDocument();
});
});
Loading
Loading