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
96 changes: 50 additions & 46 deletions cli.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,74 @@
#!/usr/bin/env bun
import { mkdir, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { spawn } from "node:child_process";

import { mkdir, writeFile } from "node:fs/promises"
import { spawn } from "node:child_process"
const EXAMPLE_TEST = `import { expect, test } from "bun:test";

const EXAMPLE_TEST = `import { expect, test } from "bun:test"

const testSvg = \`<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
</svg>\`

test("svg snapshot example", async () => {
// First run will create the snapshot
// Subsequent runs will compare against the saved snapshot
await expect(testSvg).toMatchSvgSnapshot(import.meta.path)
})
`
const testSvg = \`<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
</svg>\`;

const PRELOAD_FILE = `import "bun-match-svg"`
test("svg snapshot example", async () => {
// First run will create the snapshot
// Subsequent runs will compare against the saved snapshot
await expect(testSvg).toMatchSvgSnapshot(import.meta.path);
});
`;

const PRELOAD_FILE = `import "bun-match-svg";`;

const BUNFIG = `[test]
preload = ["./tests/fixtures/preload.ts"]`
preload = ["./tests/fixtures/preload.ts"];`;

async function installDependency() {
return new Promise((resolve, reject) => {
const install = spawn('bun', ['add', '-d', 'bun-match-svg'], {
stdio: 'inherit'
})
const install = spawn("bun", ["add", "-d", "bun-match-svg"], {
stdio: "inherit",
});

install.on('close', (code) => {
install.on("close", (code) => {
if (code === 0) {
resolve(undefined)
resolve(undefined);
} else {
reject(new Error(`Installation failed with code ${code}`))
reject(new Error(`Installation failed with code ${code}`));
}
})
})
});
});
}

async function init() {
try {
console.log("📦 Installing bun-match-svg...")
await installDependency()

await mkdir("tests/fixtures", { recursive: true })

await writeFile("tests/svg.test.ts", EXAMPLE_TEST)

await writeFile("tests/fixtures/preload.ts", PRELOAD_FILE)

await writeFile("bunfig.toml", BUNFIG)

console.log("✅ Installed bun-match-svg")
console.log("✅ Created example test in tests/svg.test.ts")
console.log("✅ Created preload file in tests/fixtures/preload.ts")
console.log("✅ Created bunfig.toml")
console.log("\n🎉 You can now run: bun test")
console.log("📦 Installing bun-match-svg...");
await installDependency();

await mkdir("tests/fixtures", { recursive: true });

const files = [
{ path: "tests/svg.test.ts", content: EXAMPLE_TEST },
{ path: "tests/fixtures/preload.ts", content: PRELOAD_FILE },
{ path: "bunfig.toml", content: BUNFIG },
];

for (const file of files) {
if (existsSync(file.path)) {
console.log(`⚠️ Skipped creating ${file.path} (file already exists)`);
continue;
}
await writeFile(file.path, file.content);
console.log(`✅ Created ${file.path}`);
}

console.log("\n🎉 You can now run: bun test");
} catch (error) {
console.error("❌ Error during initialization:", error)
process.exit(1)
console.error("❌ Error during initialization:", error);
process.exit(1);
}
}

const command = process.argv[2]
const command = process.argv[2];

if (command === "init") {
init().catch(console.error)
init().catch(console.error);
} else {
console.log("Usage: bunx bun-match-svg init")
console.log("Usage: bunx bun-match-svg init");
}
120 changes: 61 additions & 59 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,174 +1,176 @@
import { expect, type MatcherResult } from "bun:test"
import * as fs from "node:fs"
import * as path from "node:path"
import looksSame from "looks-same"
import { expect, type MatcherResult } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";
import looksSame from "looks-same";

// Single SVG Snapshot Matcher
async function toMatchSvgSnapshot(
// biome-ignore lint/suspicious/noExplicitAny: bun doesn't expose
this: any,
receivedMaybePromise: string | Promise<string>,
testPathOriginal: string,
svgName?: string,
svgName?: string
): Promise<MatcherResult> {
const received = await receivedMaybePromise
const testPath = testPathOriginal.replace(/\.test\.tsx?$/, "")
const snapshotDir = path.join(path.dirname(testPath), "__snapshots__")
const received = await receivedMaybePromise;
const testPath = testPathOriginal.replace(/\.test\.tsx?$/, "");
const snapshotDir = path.join(
path.dirname(testPathOriginal),
"__snapshots__"
);
const snapshotName = svgName
? `${svgName}.snap.svg`
: `${path.basename(testPath)}.snap.svg`
const filePath = path.join(snapshotDir, snapshotName)
: `${path.basename(testPath)}.snap.svg`;
const filePath = path.join(snapshotDir, snapshotName);

if (!fs.existsSync(snapshotDir)) {
fs.mkdirSync(snapshotDir, { recursive: true })
fs.mkdirSync(snapshotDir, { recursive: true });
}

const updateSnapshot =
process.argv.includes("--update-snapshots") ||
process.argv.includes("-u") ||
Boolean(process.env["BUN_UPDATE_SNAPSHOTS"])
Boolean(process.env["BUN_UPDATE_SNAPSHOTS"]);

if (!fs.existsSync(filePath) || updateSnapshot) {
console.log("Writing snapshot to", filePath)
fs.writeFileSync(filePath, received)
console.log("Writing snapshot to", filePath);
fs.writeFileSync(filePath, received);
return {
message: () => `Snapshot created at ${filePath}`,
pass: true,
}
};
}

const existingSnapshot = fs.readFileSync(filePath, "utf-8")
const existingSnapshot = fs.readFileSync(filePath, "utf-8");

const result: any = await looksSame(
Buffer.from(received),
Buffer.from(existingSnapshot),
{
strict: false,
tolerance: 2,
},
)
}
);

if (result.equal) {
return {
message: () => "Snapshot matches",
pass: true,
}
};
}

const diffPath = filePath.replace(".snap.svg", ".diff.png")
const diffPath = filePath.replace(".snap.svg", ".diff.png");
await looksSame.createDiff({
reference: Buffer.from(existingSnapshot),
current: Buffer.from(received),
diff: diffPath,
highlightColor: "#ff00ff",
})
});

return {
message: () => `Snapshot does not match. Diff saved at ${diffPath}`,
pass: false,
}
};
}

// Multiple SVG Snapshot Matcher
async function toMatchMultipleSvgSnapshots(
// biome-ignore lint/suspicious/noExplicitAny: bun doesn't expose
this: any,
receivedMaybePromise: string[] | Promise<string[]>,
testPathOriginal: string,
svgNames: string[],
svgNames: string[]
): Promise<MatcherResult> {
const passed: any[] = []
const failed: any[] = []
const passed: any[] = [];
const failed: any[] = [];
for (let index = 0; index < svgNames.length; index++) {
const svgName = svgNames[index]
const received = await receivedMaybePromise
const testPath = testPathOriginal.replace(/\.test\.tsx?$/, "")
const snapshotDir = path.join(path.dirname(testPath), "__snapshots__")
const svgName = svgNames[index];
const received = await receivedMaybePromise;
const testPath = testPathOriginal.replace(/\.test\.tsx?$/, "");
const snapshotDir = path.join(path.dirname(testPath), "__snapshots__");
const snapshotName = svgName
? `${svgName}.snap.svg`
: `${path.basename(testPath)}.snap.svg`
const filePath = path.join(snapshotDir, snapshotName)
: `${path.basename(testPath)}.snap.svg`;
const filePath = path.join(snapshotDir, snapshotName);

if (!fs.existsSync(snapshotDir)) {
fs.mkdirSync(snapshotDir, { recursive: true })
fs.mkdirSync(snapshotDir, { recursive: true });
}

const updateSnapshot =
process.argv.includes("--update-snapshots") ||
process.argv.includes("-u") ||
Boolean(process.env["BUN_UPDATE_SNAPSHOTS"])
Boolean(process.env["BUN_UPDATE_SNAPSHOTS"]);

if (!fs.existsSync(filePath) || updateSnapshot) {
console.log("Writing snapshot to", filePath)
fs.writeFileSync(filePath, received[index] as any)
console.log("Writing snapshot to", filePath);
fs.writeFileSync(filePath, received[index] as any);
passed.push({
message: `Snapshot ${svgName} created at ${filePath}`,
pass: true,
})
continue
});
continue;
}

const existingSnapshot = fs.readFileSync(filePath, "utf-8")
const existingSnapshot = fs.readFileSync(filePath, "utf-8");

const result: any = await looksSame(
Buffer.from(received[index] as any),
Buffer.from(existingSnapshot),
{
strict: false,
tolerance: 2,
},
)
}
);

if (result.equal) {
passed.push({
message: `Snapshot ${svgName} matches`,
pass: true,
})
continue
});
continue;
}

const diffPath = filePath.replace(".snap.svg", ".diff.png")
const diffPath = filePath.replace(".snap.svg", ".diff.png");
await looksSame.createDiff({
reference: Buffer.from(existingSnapshot),
current: Buffer.from(received[index] as any),
diff: diffPath,
highlightColor: "#ff00ff",
})
});

failed.push({
message: `Snapshot ${svgName} does not match. Diff saved at ${diffPath}`,
pass: false,
})
});
}
let aggregatedMessage = ""
let aggregatedMessage = "";
if (failed.length === 0) {
for (const result of passed) aggregatedMessage += `${result.message}\n`
for (const result of passed) aggregatedMessage += `${result.message}\n`;
return {
pass: true,
message: () => aggregatedMessage,
}
};
}
for (const result of failed) aggregatedMessage += `${result.message}\n`
for (const result of failed) aggregatedMessage += `${result.message}\n`;
return {
pass: false,
message: () => aggregatedMessage,
}
};
}

// Extend expect with the custom matchers
expect.extend({
// biome-ignore lint/suspicious/noExplicitAny:
toMatchSvgSnapshot: toMatchSvgSnapshot as any,
// biome-ignore lint/suspicious/noExplicitAny:
toMatchMultipleSvgSnapshots: toMatchMultipleSvgSnapshots as any,
})
});

declare module "bun:test" {
interface Matchers<T = unknown> {
toMatchSvgSnapshot(
testPath: string,
svgName?: string,
): Promise<MatcherResult>
svgName?: string
): Promise<MatcherResult>;
toMatchMultipleSvgSnapshots(
testPath: string,
svgNames?: string[],
): Promise<MatcherResult>
svgNames?: string[]
): Promise<MatcherResult>;
}
}
Loading
Loading