Skip to content

Commit

Permalink
test(figma-utils): add unit tests (#101)
Browse files Browse the repository at this point in the history
closes #99

## Description

Add unit tests for the `@sit-onyx/figma-utils` package which tests all
main use cases.
  • Loading branch information
larsrickert authored Jan 12, 2024
1 parent e8f6f01 commit eebf509
Show file tree
Hide file tree
Showing 10 changed files with 435 additions and 53 deletions.
5 changes: 5 additions & 0 deletions .changeset/lemon-dragons-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sit-onyx/figma-utils": patch
---

fix(parse): remove mode name if its the default Figma mode name "Mode 1"
8 changes: 6 additions & 2 deletions packages/figma-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@
}
},
"scripts": {
"build": "rimraf dist && tsc",
"@sit-onyx/figma-utils": "node ./dist/cli.js"
"build": "pnpm run '/type-check|build-only/'",
"build-only": "rimraf dist && tsc -p tsconfig.node.json --composite false",
"type-check": "tsc --noEmit -p tsconfig.vitest.json --composite false",
"@sit-onyx/figma-utils": "node ./dist/cli.js",
"test": "vitest",
"test:coverage": "pnpm run test --coverage"
},
"dependencies": {
"commander": "^11.1.0"
Expand Down
71 changes: 71 additions & 0 deletions packages/figma-utils/src/commands/import-variables.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import fs from "node:fs";
import { beforeEach, describe, expect, test, vi } from "vitest";
import * as functions from "../index.js";
import { ImportCommandOptions, importCommandAction } from "./import-variables.js";

vi.mock("node:fs");

vi.mock("../index.js");

describe("import-variables.ts", () => {
const mockOptions = {
fileKey: "test-file-key",
filename: "test-file-name",
format: "CSS",
token: "test-token",
selector: ":root",
} satisfies ImportCommandOptions;

beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(console, "log").mockImplementation(() => ({}));
vi.spyOn(process, "cwd").mockReturnValue("test-cwd");
});

test("should throw error for unknown formats", () => {
const promise = () => importCommandAction({ ...mockOptions, format: "does-not-exist" });
expect(promise).rejects.toThrowError("Unknown format: does-not-exist. Supported: CSS, SCSS");
});

test("should throw error for unknown modes", () => {
vi.spyOn(functions, "parseFigmaVariables").mockReturnValue([
{ modeName: "test-mode-1", variables: {} },
]);

const promise = () =>
importCommandAction({
...mockOptions,
modes: ["test-mode-1", "does-not-exist"],
});
expect(promise).rejects.toThrowError(
'Mode "does-not-exist" not found. Available modes: "test-mode-1"',
);
});

test("should generate variables", async () => {
vi.spyOn(functions, "parseFigmaVariables").mockReturnValue([
{ modeName: "test-mode-1", variables: {} },
{ modeName: "test-mode-2", variables: {} },
{ modeName: "test-mode-3", variables: {} },
]);

vi.spyOn(functions, "generateAsCSS").mockReturnValue("mock-css-file-content");

await importCommandAction({ ...mockOptions, modes: ["test-mode-1", "test-mode-2"] });

expect(functions.fetchFigmaVariables).toHaveBeenCalledOnce();
expect(functions.parseFigmaVariables).toHaveBeenCalledOnce();
expect(functions.generateAsCSS).toHaveBeenCalledTimes(2);
expect(fs.writeFileSync).toHaveBeenCalledTimes(2);

expect(fs.writeFileSync).toHaveBeenCalledWith(
"test-cwd/test-file-name-test-mode-1.css",
"mock-css-file-content",
);

expect(fs.writeFileSync).toHaveBeenCalledWith(
"test-cwd/test-file-name-test-mode-2.css",
"mock-css-file-content",
);
});
});
91 changes: 48 additions & 43 deletions packages/figma-utils/src/commands/import-variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
parseFigmaVariables,
} from "../index.js";

type ImportCommandOptions = {
export type ImportCommandOptions = {
fileKey: string;
token: string;
filename: string;
Expand Down Expand Up @@ -42,57 +42,62 @@ export const importCommand = new Command("import-variables")
'CSS selector to use for the CSS format. The mode name will be added to the selector if it is set to something other than ":root", e.g. for the mode named "dark", passing the selector "html" will result in "html.dark"',
":root",
)
.action(async (options: ImportCommandOptions) => {
const generators = {
CSS: (data: ParsedVariable) => generateAsCSS(data, options.selector),
SCSS: generateAsSCSS,
};
.action(importCommandAction);

if (!(options.format in generators)) {
throw new Error(
`Unknown format: ${options.format}. Supported: ${Object.keys(generators).join(", ")}`,
);
}
/**
* Action to run when executing the import action. Only intended to be called manually for testing.
*/
export async function importCommandAction(options: ImportCommandOptions) {
const generators = {
CSS: (data: ParsedVariable) => generateAsCSS(data, options.selector),
SCSS: generateAsSCSS,
};

console.log("Fetching variables from Figma API...");
const data = await fetchFigmaVariables(options.fileKey, options.token);
if (!(options.format in generators)) {
throw new Error(
`Unknown format: ${options.format}. Supported: ${Object.keys(generators).join(", ")}`,
);
}

console.log("Parsing Figma variables...");
const parsedVariables = parseFigmaVariables(data);
console.log("Fetching variables from Figma API...");
const data = await fetchFigmaVariables(options.fileKey, options.token);

if (options.modes?.length) {
// verify that all modes are found
for (const mode of options.modes) {
if (parsedVariables.find((i) => i.modeName === mode)) continue;
console.log("Parsing Figma variables...");
const parsedVariables = parseFigmaVariables(data);

const availableModes = parsedVariables
.map((i) => i.modeName ?? DEFAULT_MODE_NAME)
.map((mode) => `"${mode}"`);
if (options.modes?.length) {
// verify that all modes are found
for (const mode of options.modes) {
if (parsedVariables.find((i) => i.modeName === mode)) continue;

throw new Error(
`Mode "${mode}" not found. Available modes: ${Object.values(availableModes).join(", ")}`,
);
}
}
const availableModes = parsedVariables
.map((i) => i.modeName ?? DEFAULT_MODE_NAME)
.map((mode) => `"${mode}"`);

const outputDirectory = options.dir ?? process.cwd();
const filename = options.filename ?? "variables";
throw new Error(
`Mode "${mode}" not found. Available modes: ${Object.values(availableModes).join(", ")}`,
);
}
}

console.log(`Generating ${options.format} variables...`);
const outputDirectory = options.dir ?? process.cwd();
const filename = options.filename ?? "variables";

parsedVariables.forEach((data) => {
// if the user passed specific modes to be exported, we will only generate those
// otherwise all modes will be exported.
// the default mode (undefined data.modeName) is always generated because its mode name can
// not be specified by the designer in Figma
const isModeIncluded =
!options.modes?.length || !data.modeName || options.modes.includes(data.modeName);
if (!isModeIncluded) return;
console.log(`Generating ${options.format} variables...`);

const baseName = data.modeName ? `${filename}-${data.modeName}` : filename;
const fullPath = path.join(outputDirectory, `${baseName}.${options.format.toLowerCase()}`);
fs.writeFileSync(fullPath, generators[options.format as keyof typeof generators](data));
});
parsedVariables.forEach((data) => {
// if the user passed specific modes to be exported, we will only generate those
// otherwise all modes will be exported.
// the default mode (undefined data.modeName) is always generated because its mode name can
// not be specified by the designer in Figma
const isModeIncluded =
!options.modes?.length || !data.modeName || options.modes.includes(data.modeName);
if (!isModeIncluded) return;

console.log("Done.");
const baseName = data.modeName ? `${filename}-${data.modeName}` : filename;
const fullPath = path.join(outputDirectory, `${baseName}.${options.format.toLowerCase()}`);
fs.writeFileSync(fullPath, generators[options.format as keyof typeof generators](data));
});

console.log("Done.");
}
64 changes: 64 additions & 0 deletions packages/figma-utils/src/utils/generate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ParsedVariable } from "src/index.js";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { generateAsCSS, generateAsSCSS } from "./generate.js";

describe("generate.ts", () => {
const mockData = {
modeName: "test-mode-1",
variables: {
"test-1": "#ffffff",
"test-2": "1rem",
"test-3": "{test-2}",
},
} satisfies ParsedVariable;

beforeEach(() => {
vi.setSystemTime(new Date(2024, 0, 7, 13, 42));
});

test("should generate as CSS", () => {
const fileContent = generateAsCSS(mockData);

expect(fileContent).toBe(`/**
* Do not edit directly.
* This file contains the specific variables for the "test-mode-1" theme.
* Imported from Figma API on Sun, 07 Jan 2024 12:42:00 GMT
*/
:root {
--test-1: #ffffff;
--test-2: 1rem;
--test-3: var(--test-2);
}
`);
});

test("should generate as CSS with custom selector", () => {
const fileContent = generateAsCSS(mockData, "html");

expect(fileContent).toBe(`/**
* Do not edit directly.
* This file contains the specific variables for the "test-mode-1" theme.
* Imported from Figma API on Sun, 07 Jan 2024 12:42:00 GMT
*/
html.test-mode-1 {
--test-1: #ffffff;
--test-2: 1rem;
--test-3: var(--test-2);
}
`);
});

test("should generate as SCSS", () => {
const fileContent = generateAsSCSS(mockData);

expect(fileContent).toBe(`/**
* Do not edit directly.
* This file contains the specific variables for the "test-mode-1" theme.
* Imported from Figma API on Sun, 07 Jan 2024 12:42:00 GMT
*/
$test-1: #ffffff;
$test-2: 1rem;
$test-3: $test-2;
`);
});
});
Loading

0 comments on commit eebf509

Please sign in to comment.