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
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ Thanks for contributing! Any contributions you make are greatly appreciated (and
5. Write tests to cover your changes and ensure they pass by running `npm run test`.
6. Commit your changes with a clear and concise commit message. Follow good Git commit message practices, such as using prefixes like `fix`, `feat`, `chore`, `test`, etc.

## Testing

- Unit/UI tests: `npm run test`
- E2E tests (Playwright): `npm run test:e2e`
- Optional UI runner: `npm run test:e2e:ui`

When adding a new utility, prefer:
- One Jest UI smoke test (render + heading).
- One unit test for the conversion utility (if it lives in `components/utils`).
- One Playwright E2E flow using fixtures under `tests/fixtures`.

## Submitting a Pull Request

1. Push your branch to your forked repository.
Expand Down
91 changes: 91 additions & 0 deletions __tests__/pages/utilities/utilities.smoke.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { render, screen } from "@testing-library/react";

import Base64Encoder from "../../../pages/utilities/base-64-encoder";
import Base64ToImage from "../../../pages/utilities/base64-to-image";
import CameraUtility from "../../../pages/utilities/cam";
import CssInliner from "../../../pages/utilities/css-inliner-for-email";
import CssUnitsConverter from "../../../pages/utilities/css-units-converter";
import CSVtoJSON from "../../../pages/utilities/csv-to-json";
import CurlToFetch from "../../../pages/utilities/curl-to-javascript-fetch";
import EnvToToml from "../../../pages/utilities/env-to-netlify-toml";
import HARFileViewer from "../../../pages/utilities/har-file-viewer";
import HashGenerator from "../../../pages/utilities/hash-generator";
import HexToRGB from "../../../pages/utilities/hex-to-rgb";
import ImageResizer from "../../../pages/utilities/image-resizer";
import ImageToBase64 from "../../../pages/utilities/image-to-base64";
import InternetSpeedTest from "../../../pages/utilities/internet-speed-test";
import JSONFormatter from "../../../pages/utilities/json-formatter";
import JSONtoCSV from "../../../pages/utilities/json-to-csv";
import JSONtoYAML from "../../../pages/utilities/json-to-yaml";
import JWTParser from "../../../pages/utilities/jwt-parser";
import LoremIpsumGenerator from "../../../pages/utilities/lorem-ipsum-generator";
import NumberBaseChanger from "../../../pages/utilities/number-base-changer";
import QueryParamsToJSON from "../../../pages/utilities/query-params-to-json";
import RandomStringGenerator from "../../../pages/utilities/random-string-generator";
import RegexTester from "../../../pages/utilities/regex-tester";
import RGBToHex from "../../../pages/utilities/rgb-to-hex";
import SQLMinifier from "../../../pages/utilities/sql-minifier";
import SVGViewer from "../../../pages/utilities/svg-viewer";
import TimestampToDate from "../../../pages/utilities/timestamp-to-date";
import URLEncoder from "../../../pages/utilities/url-encoder";
import UuidGenerator from "../../../pages/utilities/uuid-generator";
import WcagColorContrastChecker from "../../../pages/utilities/wcag-color-contrast-checker";
import WebpConverter from "../../../pages/utilities/webp-converter";
import XMLtoJSON from "../../../pages/utilities/xml-to-json";
import YAMLtoJSON from "../../../pages/utilities/yaml-to-json";

jest.mock("@cloudflare/speedtest", () => {
return class MockSpeedTestEngine {
results = {
getSummary: () => ({}),
};
play = jest.fn();
pause = jest.fn();
onResultsChange = () => {};
onFinish = () => {};
onError = () => {};
};
});

const utilities = [
Base64Encoder,
Base64ToImage,
CameraUtility,
CssInliner,
CssUnitsConverter,
CSVtoJSON,
CurlToFetch,
EnvToToml,
HARFileViewer,
HashGenerator,
HexToRGB,
ImageResizer,
ImageToBase64,
InternetSpeedTest,
JSONFormatter,
JSONtoCSV,
JSONtoYAML,
JWTParser,
LoremIpsumGenerator,
NumberBaseChanger,
QueryParamsToJSON,
RandomStringGenerator,
RegexTester,
RGBToHex,
SQLMinifier,
SVGViewer,
TimestampToDate,
URLEncoder,
UuidGenerator,
WcagColorContrastChecker,
WebpConverter,
XMLtoJSON,
YAMLtoJSON,
];

describe("utilities smoke", () => {
test.each(utilities)("renders utility page", (UtilityPage) => {
render(<UtilityPage />);
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
});
});
4 changes: 4 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const customJestConfig = {
"^.+\\.module\\.(css|sass|scss)$",
],
modulePathIgnorePatterns: ["<rootDir>/build"],
testPathIgnorePatterns: ["<rootDir>/tests/e2e/"],
moduleNameMapper: {
"^curlconverter$": "<rootDir>/tests/__mocks__/curlconverter.ts",
},
testMatch: [
"**/__tests__/**/*.+(ts|tsx|js)",
"**/?(*.)+(spec|test).+(ts|tsx|js)",
Expand Down
14 changes: 14 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,17 @@ jest.mock("next/navigation", () => ({
refresh: jest.fn(),
}),
}));

if (!globalThis.crypto) {
Object.defineProperty(globalThis, "crypto", {
value: {},
configurable: true,
});
}

if (!globalThis.crypto.randomUUID) {
Object.defineProperty(globalThis.crypto, "randomUUID", {
value: () => "00000000-0000-4000-8000-000000000000",
configurable: true,
});
}
64 changes: 64 additions & 0 deletions package-lock.json

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

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"start": "next start",
"lint": "next lint",
"test": "jest",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"format": "prettier --write '**/*.{js,jsx,ts,tsx,json,css,scss,md}'",
"format:check": "prettier --check '**/*.{js,jsx,ts,tsx,json,css,scss,md}'"
},
Expand Down Expand Up @@ -41,6 +43,7 @@
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@playwright/test": "^1.58.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.6.1",
Expand Down
25 changes: 25 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
testDir: "tests/e2e",
timeout: 30_000,
expect: {
timeout: 10_000,
},
use: {
baseURL: "http://localhost:3000",
trace: "retain-on-failure",
},
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
});
1 change: 1 addition & 0 deletions tests/__mocks__/curlconverter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const toJavaScript = () => "fetch('https://example.com')";
99 changes: 99 additions & 0 deletions tests/e2e/utilities.happy-path.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { test, expect } from "@playwright/test";
import { fixturePath, readFixture } from "../helpers/fixtures";
import { gotoUtility } from "../helpers/playwright";

test.describe("utilities happy paths", () => {
test("csv-to-json converts input", async ({ page }) => {
await gotoUtility(page, "csv-to-json");
const csv = readFixture("csv/sample.csv");
await page.getByPlaceholder("Paste or drag and drop a CSV file").fill(csv);
const outputs = page.locator("textarea");
await expect(outputs.nth(1)).toHaveValue(/Name/);
});

test("json-formatter formats JSON", async ({ page }) => {
await gotoUtility(page, "json-formatter");
const json = readFixture("json/sample.json");
await page.getByPlaceholder("Paste JSON here").fill(json);
const outputs = page.locator("textarea");
await expect(outputs.nth(1)).toHaveValue(/"name":/);
});

test("url-encoder encodes text", async ({ page }) => {
await gotoUtility(page, "url-encoder");
const raw = readFixture("url/sample.txt");
await page.getByPlaceholder("Paste here").fill(raw);
const encoded = encodeURIComponent(raw);
const outputs = page.locator("textarea");
await expect(outputs.nth(1)).toHaveValue(encoded);
});

test("yaml-to-json converts YAML", async ({ page }) => {
await gotoUtility(page, "yaml-to-json");
const yaml = readFixture("yaml/sample.yaml");
await page.getByPlaceholder("Paste YAML here").fill(yaml);
const outputs = page.locator("textarea");
await expect(outputs.nth(1)).toHaveValue(/"name":/);
});

test("xml-to-json converts XML", async ({ page }) => {
await gotoUtility(page, "xml-to-json");
const xml = readFixture("xml/sample.xml");
await page.getByPlaceholder("Paste XML here").fill(xml);
const outputs = page.locator("textarea");
await expect(outputs.nth(1)).toHaveValue(/"root"/);
});

test("env-to-netlify-toml converts env", async ({ page }) => {
await gotoUtility(page, "env-to-netlify-toml");
const env = readFixture("env/sample.env");
await page.getByPlaceholder("Paste here").fill(env);
const outputs = page.locator("textarea");
await expect(outputs.nth(1)).toHaveValue(/\[context\.production\]/);
});

test("har-file-viewer accepts har upload", async ({ page }) => {
await gotoUtility(page, "har-file-viewer");
await page
.getByTestId("input")
.setInputFiles(fixturePath("har/sample.har"));
await expect(
page.getByText("https://example.com/api/test")
).toBeVisible();
});

test("svg-viewer accepts svg upload", async ({ page }) => {
await gotoUtility(page, "svg-viewer");
await page.getByRole("tab", { name: "Upload SVG File" }).click();
await page
.locator('input[type="file"]')
.setInputFiles(fixturePath("svg/sample.svg"));
await expect(page.getByLabel("Uploaded SVG code")).toHaveValue(/<svg/);
});

test("uuid-generator creates UUID", async ({ page }) => {
await gotoUtility(page, "uuid-generator");
const uuidField = page.locator("input[readonly]");
await expect(uuidField).toHaveValue(
/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i
);
const initialValue = await uuidField.inputValue();
await page.getByRole("button", { name: "Generate New UUID" }).click();
await expect(uuidField).not.toHaveValue(initialValue);
});

test("random-string-generator creates string", async ({ page }) => {
await gotoUtility(page, "random-string-generator");
await page.getByRole("button", { name: "Generate" }).click();
const output = page.getByPlaceholder(
"Click 'Generate' to create a cryptographically strong random string."
);
await expect(output).toHaveValue(/.{4,}/);
});

test("lorem-ipsum-generator renders text", async ({ page }) => {
await gotoUtility(page, "lorem-ipsum-generator");
const output = page.locator("textarea").first();
await expect(output).toHaveValue(/.{4,}/);
});
});
Loading