Release Token Tools 0.1.0
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
for (const path of ["/", "/deep/nested/token/"]) {
|
||||
test(`edits and exports locally at ${path}`, async ({ page }) => {
|
||||
const external: string[] = [];
|
||||
page.on("request", (request) => {
|
||||
if (!request.url().startsWith("http://127.0.0.1:4183"))
|
||||
external.push(request.url());
|
||||
});
|
||||
await page.goto(path);
|
||||
await expect(
|
||||
page.getByRole("heading", {
|
||||
name: "One token source, honest platform output.",
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(page.getByLabel("Export output")).toContainText(
|
||||
"--color-brand",
|
||||
);
|
||||
await page
|
||||
.getByRole("combobox", { name: "Theme", exact: true })
|
||||
.selectOption("dark");
|
||||
await expect(
|
||||
page.locator(".resolved-table tr").filter({ hasText: "color.text" }),
|
||||
).toContainText("#f4f2ff");
|
||||
expect(external).toEqual([]);
|
||||
});
|
||||
}
|
||||
|
||||
test("keeps the installed application available offline", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page.evaluate(async () => navigator.serviceWorker.ready);
|
||||
await page.reload();
|
||||
await context.setOffline(true);
|
||||
try {
|
||||
await page.reload();
|
||||
await expect(
|
||||
page.getByRole("heading", {
|
||||
name: "One token source, honest platform output.",
|
||||
}),
|
||||
).toBeVisible();
|
||||
} finally {
|
||||
await context.setOffline(false);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Workbench } from "../../src/components/Workbench";
|
||||
|
||||
describe("Workbench", () => {
|
||||
it("switches themes and keeps aliases resolved", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Workbench />);
|
||||
await user.selectOptions(screen.getByLabelText("Theme"), "dark");
|
||||
expect(screen.getByText("#f4f2ff")).toBeVisible();
|
||||
expect(screen.getByText(/Sets: core → dark/u)).toBeVisible();
|
||||
});
|
||||
|
||||
it("does not replace the active document with invalid JSON", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Workbench />);
|
||||
const source = screen.getByLabelText("Token document JSON");
|
||||
fireEvent.change(source, { target: { value: "{" } });
|
||||
await user.click(screen.getByRole("button", { name: "Apply JSON" }));
|
||||
expect(screen.getByRole("alert")).toBeVisible();
|
||||
expect(screen.getAllByText("color.brand").length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
deleteToken,
|
||||
exportTokens,
|
||||
parseTokenDocument,
|
||||
resolveTheme,
|
||||
updateToken,
|
||||
} from "../../src/core/tokens";
|
||||
|
||||
const themed = JSON.stringify({
|
||||
$sets: {
|
||||
core: {
|
||||
color: {
|
||||
$type: "color",
|
||||
brand: { $value: "#112233" },
|
||||
link: { $value: "{color.brand}" },
|
||||
},
|
||||
space: { sm: { $type: "dimension", $value: "8px" } },
|
||||
},
|
||||
dark: { color: { $type: "color", brand: { $value: "#ddeeff" } } },
|
||||
},
|
||||
$themes: { light: ["core"], dark: ["core", "dark"] },
|
||||
});
|
||||
|
||||
describe("token model", () => {
|
||||
it("composes sets and resolves aliases after overrides", () => {
|
||||
const result = resolveTheme(parseTokenDocument(themed), "dark");
|
||||
expect(
|
||||
result.tokens.find((token) => token.path === "color.link")?.resolvedValue,
|
||||
).toBe("#ddeeff");
|
||||
expect(result.diagnostics).toEqual([]);
|
||||
});
|
||||
|
||||
it("detects cycles and missing aliases", () => {
|
||||
const document = parseTokenDocument(
|
||||
'{"a":{"$value":"{b}"},"b":{"$value":"{a}"},"c":{"$value":"{missing}"}}',
|
||||
);
|
||||
const messages = resolveTheme(document, "default")
|
||||
.diagnostics.map((item) => item.message)
|
||||
.join(" ");
|
||||
expect(messages).toMatch(/cycle/u);
|
||||
expect(messages).toMatch(/does not exist/u);
|
||||
});
|
||||
|
||||
it("rejects malformed wrappers and non-standard hex lengths", () => {
|
||||
expect(() => parseTokenDocument('{"$sets":[]}')).toThrow(
|
||||
/\$sets must be an object/u,
|
||||
);
|
||||
const document = parseTokenDocument(
|
||||
'{"five":{"$type":"color","$value":"#12345"},"seven":{"$type":"color","$value":"#1234567"}}',
|
||||
);
|
||||
expect(resolveTheme(document, "default").diagnostics).toHaveLength(2);
|
||||
expect(() => parseTokenDocument('{"$themes":{}}')).toThrow(
|
||||
/requires a \$sets/u,
|
||||
);
|
||||
expect(() =>
|
||||
parseTokenDocument(
|
||||
'{"leaf":{"$value":"x","hidden":{"$value":"ignored"}}}',
|
||||
),
|
||||
).toThrow(/cannot also contain child/u);
|
||||
});
|
||||
|
||||
it("exports CSS and reports platform loss", () => {
|
||||
const tokens = resolveTheme(parseTokenDocument(themed), "light").tokens;
|
||||
expect(exportTokens("css", tokens).content).toContain(
|
||||
"--color-brand: #112233;",
|
||||
);
|
||||
const composite = parseTokenDocument(
|
||||
'{"text":{"$type":"typography","$value":{"fontFamily":"Inter","fontSize":"16px"}}}',
|
||||
);
|
||||
expect(
|
||||
exportTokens("android", resolveTheme(composite, "default").tokens)
|
||||
.losses[0],
|
||||
).toMatch(/project-specific/u);
|
||||
});
|
||||
|
||||
it("quotes generated strings and typography and reports name collisions", () => {
|
||||
const document = parseTokenDocument(
|
||||
JSON.stringify({
|
||||
danger: { $type: "string", $value: 'x"; } --owned: red; /*' },
|
||||
text: {
|
||||
$type: "typography",
|
||||
$value: {
|
||||
fontFamily: 'Evil"; } body { color: red; /*',
|
||||
fontSize: "16px",
|
||||
},
|
||||
},
|
||||
a: { b: { $type: "string", $value: "first" } },
|
||||
"a-b": { $type: "string", $value: "second" },
|
||||
}),
|
||||
);
|
||||
const result = exportTokens(
|
||||
"css",
|
||||
resolveTheme(document, "default").tokens,
|
||||
);
|
||||
expect(result.content).toContain('--danger: "x\\"; } --owned: red; /*";');
|
||||
expect(result.content).toContain('"Evil\\"; } body { color: red; /*"');
|
||||
expect(result.losses.join(" ")).toMatch(/identifier a-b collides/u);
|
||||
});
|
||||
|
||||
it("edits and deletes nested tokens", () => {
|
||||
const document = parseTokenDocument(themed);
|
||||
const updated = updateToken(
|
||||
document,
|
||||
"core",
|
||||
"space.sm",
|
||||
"space.md",
|
||||
"dimension",
|
||||
"16px",
|
||||
);
|
||||
expect(
|
||||
parseTokenDocument(updated).sets.core?.some(
|
||||
(token) => token.path === "space.md",
|
||||
),
|
||||
).toBe(true);
|
||||
const removed = deleteToken(
|
||||
parseTokenDocument(updated),
|
||||
"core",
|
||||
"space.md",
|
||||
);
|
||||
expect(
|
||||
parseTokenDocument(removed).sets.core?.some(
|
||||
(token) => token.path === "space.md",
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("refuses to create a path through an existing token leaf", () => {
|
||||
const document = parseTokenDocument(
|
||||
'{"base":{"$type":"string","$value":"leaf"}}',
|
||||
);
|
||||
expect(() =>
|
||||
updateToken(document, "default", null, "base.child", "string", "x"),
|
||||
).toThrow(/crosses existing token base/u);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user