Skip to content
Merged
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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
node_modules/
dist/
tmp/
test-results/
playwright-report/
e2e/site/playwright-report/
.env
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,windows
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,windows
Expand Down Expand Up @@ -85,4 +89,4 @@ $RECYCLE.BIN/
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,windows

*.tgz
colbrush-*.tgz
colbrush-*.tgz
12 changes: 12 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Exclude development and test assets from published packages
e2e/
scripts/
test-results/
tmp/

# Ignore repository-specific artifacts
*.tgz
*.log
.DS_Store
.vscode/
node_modules/
8 changes: 8 additions & 0 deletions e2e/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "colbrush-cli-e2e",
"private": true,
"type": "module",
"scripts": {
"test": "node --test tests/*.test.mjs"
}
}
152 changes: 152 additions & 0 deletions e2e/cli/tests/cli.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { before, after, test } from 'node:test';
import assert from 'node:assert/strict';
import path from 'node:path';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { spawn } from 'node:child_process';
import { mkdtemp, readFile, cp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '../../..');
const cliEntry = path.join(repoRoot, 'dist/cli.cjs');
const fixtureCss = path.join(repoRoot, 'e2e/site/src/index.css');
const packageVersion = JSON.parse(
readFileSync(path.join(repoRoot, 'package.json'), 'utf8')
).version;

const sandboxes = [];

const runProcess = (command, args, options = {}) =>
new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd: options.cwd ?? repoRoot,
env: { ...process.env, ...options.env },
stdio: options.inheritStdio ? 'inherit' : 'pipe',
});

if (options.inheritStdio) {
child.on('exit', (code) => {
resolve({ code, stdout: '', stderr: '' });
});
child.on('error', reject);
return;
}

let stdout = '';
let stderr = '';

child.stdout?.on('data', (chunk) => {
stdout += chunk.toString();
});
child.stderr?.on('data', (chunk) => {
stderr += chunk.toString();
});

child.on('exit', (code) => {
resolve({ code, stdout, stderr });
});
child.on('error', reject);
});

before(async () => {
console.log('🛠️ 라이브러리 빌드를 준비 중입니다...\n');
const buildResult = await runProcess('pnpm', ['build'], {
inheritStdio: true,
});
if (buildResult.code !== 0) {
throw new Error('Failed to build library before CLI tests');
}
console.log('\n✅ 라이브러리 빌드 완료\n');
});

after(async () => {
await Promise.all(
sandboxes.map((dir) => rm(dir, { recursive: true, force: true }))
);
});

async function createSandbox() {
const sandboxRoot = await mkdtemp(path.join(tmpdir(), 'colbrush-cli-'));
const targetCssPath = path.join(sandboxRoot, 'input.css');
await cp(fixtureCss, targetCssPath);
sandboxes.push(sandboxRoot);
return { sandboxRoot, cssPath: targetCssPath };
}

async function runCli(args, options) {
return runProcess(process.execPath, [cliEntry, ...args], options);
}

test('prints version information with --version flag', async () => {
const result = await runCli(['--version']);

assert.equal(result.code, 0);
assert.ok(
result.stdout.includes(`Colbrush v${packageVersion}`),
`CLI stdout should contain version banner, got: ${result.stdout}`
);
});

test('displays usage instructions with --help', async () => {
const result = await runCli(['--help']);

assert.equal(result.code, 0);
const normalized = result.stdout.replace(/\u001B\[[0-9;]*m/g, '');
assert.ok(
normalized.includes('USAGE'),
'도움말에는 "USAGE" 섹션이 포함되어야 합니다'
);
assert.ok(
normalized.includes('Generate color-blind accessible themes'),
'도움말에는 generate 명령 설명이 포함되어야 합니다'
);
});

test('generates themed CSS and report for a valid input file', async () => {
const { sandboxRoot, cssPath } = await createSandbox();
const reportPath = path.join(sandboxRoot, 'report.json');

const result = await runCli(
['generate', `--css=${cssPath}`, `--json=${reportPath}`],
{ cwd: sandboxRoot }
);

assert.equal(
result.code,
0,
`CLI exited with non-zero code: ${result.stderr}`
);
assert.ok(
result.stdout.includes('All themes generated successfully'),
'CLI는 성공적으로 완료되었다는 메시지를 출력해야 합니다'
);

const cssOutput = await readFile(cssPath, 'utf8');
assert.match(
cssOutput,
/\[data-theme='protanopia']\s*{[\s\S]+--color-primary-500:/,
'CSS file should contain the protanopia theme block'
);
assert.match(
cssOutput,
/\[data-theme='deuteranopia']\s*{[\s\S]+--color-primary-500:/,
'CSS file should contain the deuteranopia theme block'
);
assert.match(
cssOutput,
/\[data-theme='tritanopia']\s*{[\s\S]+--color-primary-500:/,
'CSS file should contain the tritanopia theme block'
);

const jsonReportRaw = await readFile(reportPath, 'utf8');
const jsonReport = JSON.parse(jsonReportRaw);

assert.equal(jsonReport.input, cssPath);
assert.equal(jsonReport.exitCode, 0);
assert.equal(jsonReport.themes.length, 3);
assert.ok(
jsonReport.variables.processed >= 4,
`Expected at least 4 processed variables, got ${jsonReport.variables.processed}`
);
});
12 changes: 12 additions & 0 deletions e2e/site/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Colbrush E2E Playground</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
28 changes: 28 additions & 0 deletions e2e/site/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "colbrush-e2e-site",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test:e2e": "node ../../scripts/run-site-e2e.mjs"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
"colbrush": "link:../../",
"playwright": "^1.56.1",
"playwright-core": "^1.56.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.1.16"
},
"devDependencies": {
"@playwright/test": "1.56.1",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^5.1.0",
"typescript": "^5.8.0",
"vite": "^6.0.0"
}
}
31 changes: 31 additions & 0 deletions e2e/site/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { defineConfig, devices } from '@playwright/test';

const useExternalServer =
process.env.PLAYWRIGHT_EXTERNAL_SERVER === '1' ||
process.env.PLAYWRIGHT_SKIP_WEB_SERVER === '1';

export default defineConfig({
testDir: './tests',
fullyParallel: false,
retries: process.env.CI ? 2 : 0,
reporter: [['list'], ['html', { outputFolder: 'playwright-report', open: 'never' }]],
use: {
baseURL: 'http://127.0.0.1:4173',
trace: 'retain-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: useExternalServer
? undefined
: {
command: 'pnpm dev -- --host 127.0.0.1 --port 4173',
url: 'http://127.0.0.1:4173',
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
stderr: 'pipe',
},
});
Loading