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
40 changes: 29 additions & 11 deletions .github/workflows/cross-platform-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ on:

jobs:
test:
timeout-minutes: 90
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: ['20.x', '22.x']
fail-fast: false
fail-fast: true

runs-on: ${{ matrix.os }}
name: Test on ${{ matrix.os }} (Node ${{ matrix.node-version }})
env:
CI: true
CRF_NPM_INSTALL_TIMEOUT_MS: '600000'
CRF_NPM_BUILD_TIMEOUT_MS: '600000'
CRF_TEST_HOOK_TIMEOUT_MS: '300000'
CRF_CLI_TEST_TIMEOUT_MS: '30000'

steps:
- name: Checkout code
Expand All @@ -33,28 +40,36 @@ jobs:
run: npm run build

- name: Run unit tests
run: npm test
timeout-minutes: 20
run: npm test -- --bail=1 src/__tests__/ --exclude "src/__tests__/integration/**"

- name: Run integration tests
run: npm test -- src/__tests__/integration/
timeout-minutes: 45
run: npm test -- --bail=1 src/__tests__/integration/

- name: Run cross-platform tests
run: npm test -- src/__tests__/integration/cross-platform-cli.test.ts
timeout-minutes: 15
run: npm test -- --bail=1 src/__tests__/integration/cross-platform-cli.test.ts

- name: Run E2E CLI tests
run: npm test -- src/__tests__/integration/e2e-cli.test.ts
timeout-minutes: 15
run: npm test -- --bail=1 src/__tests__/integration/e2e-cli.test.ts

- name: Run E2E scenario tests
run: npm test -- src/__tests__/integration/e2e-scenarios.test.ts
timeout-minutes: 20
run: npm test -- --bail=1 src/__tests__/integration/e2e-scenarios.test.ts

- name: Run config builder comprehensive tests
run: npm test -- src/__tests__/config-builder.test.ts
timeout-minutes: 15
run: npm test -- --bail=1 src/__tests__/config-builder.test.ts

- name: Run template system comprehensive tests
run: npm test -- src/__tests__/integration/template-loading.test.ts
timeout-minutes: 15
run: npm test -- --bail=1 src/__tests__/integration/template-loading.test.ts

- name: Run generator comprehensive tests
run: npm test -- src/__tests__/integration/generator.test.ts
timeout-minutes: 15
run: npm test -- --bail=1 src/__tests__/integration/generator.test.ts

- name: Test CLI command (Unix)
if: runner.os != 'Windows'
Expand All @@ -68,8 +83,8 @@ jobs:
shell: powershell
run: |
# Test that the CLI can be invoked
node dist/index.js --version -or $true
node dist/index.js --help -or $true
try { node dist/index.js --version } catch { $true | Out-Null }
try { node dist/index.js --help } catch { $true | Out-Null }

- name: Upload coverage
if: matrix.os == 'ubuntu-latest'
Expand All @@ -80,6 +95,7 @@ jobs:
name: codecov-umbrella

e2e-scaffold-test:
timeout-minutes: 30
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
Expand Down Expand Up @@ -129,6 +145,7 @@ jobs:

lint:
runs-on: ubuntu-latest
timeout-minutes: 15
name: Lint and Format Check

steps:
Expand All @@ -150,6 +167,7 @@ jobs:
build-artifacts:
needs: [test, e2e-scaffold-test, lint]
runs-on: ubuntu-latest
timeout-minutes: 15
name: Verify Build Artifacts

steps:
Expand Down
19 changes: 8 additions & 11 deletions src/__tests__/cli-index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { basename } from 'path';
import type { PromptAnswers } from '../cli/prompts';

const { promptForProjectDetailsMock, generateProjectMock } = vi.hoisted(() => ({
Expand Down Expand Up @@ -55,7 +56,7 @@ describe('CLI Index', () => {
);

expect(config.name).toBe('test-app');
expect(config.path).toContain('/test-app');
expect(basename(config.path)).toBe('test-app');
expect(config.runtime).toBe('nextjs');
expect(config.language).toBe('javascript');
expect(config.styling.solution).toBe('styled-components');
Expand Down Expand Up @@ -120,11 +121,9 @@ describe('CLI Index', () => {
it('should exit with code 1 when generation fails', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(((code?: number) => {
throw new Error(`exit ${code}`);
}) as never);
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => {
throw new Error(`exit ${code}`);
}) as never);

promptForProjectDetailsMock.mockResolvedValue(createAnswers());
generateProjectMock.mockResolvedValue({
Expand All @@ -145,11 +144,9 @@ describe('CLI Index', () => {

it('should exit with code 0 when user cancels prompt', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
const exitSpy = vi
.spyOn(process, 'exit')
.mockImplementation(((code?: number) => {
throw new Error(`exit ${code}`);
}) as never);
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => {
throw new Error(`exit ${code}`);
}) as never);

promptForProjectDetailsMock.mockRejectedValue(new Error('User force closed the prompt'));

Expand Down
113 changes: 54 additions & 59 deletions src/__tests__/integration/build-verification.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { existsSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { existsSync, mkdirSync, realpathSync, rmSync } from 'fs';
import { join } from 'path';
import { afterEach, describe, expect, it } from 'vitest';
import { execa } from 'execa';
Expand All @@ -13,7 +12,14 @@ import { ProjectGenerator } from '../../generator/index.js';
*/

function getTempProjectPath(name: string): string {
return join(tmpdir(), `react-setup-build-${name}-${Date.now()}`);
const baseDir = join(process.cwd(), '.tmp-test-projects');
if (!existsSync(baseDir)) {
mkdirSync(baseDir, { recursive: true });
}
return join(
baseDir,
`react-setup-build-${name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
);
}

function cleanupProject(path: string): void {
Expand Down Expand Up @@ -46,13 +52,39 @@ function createConfig(name: string, overrides: Partial<ProjectConfig>): ProjectC
};
}

const NPM_INSTALL_TIMEOUT_MS = Number(process.env.CRF_NPM_INSTALL_TIMEOUT_MS ?? 600000);
const NPM_BUILD_TIMEOUT_MS = Number(process.env.CRF_NPM_BUILD_TIMEOUT_MS ?? 600000);
const TEST_HOOK_TIMEOUT_MS = Number(process.env.CRF_TEST_HOOK_TIMEOUT_MS ?? 300000);

async function installDependencies(projectPath: string) {
return execa('npm', ['install', '--no-audit', '--no-fund'], {
cwd: getCommandCwd(projectPath),
timeout: NPM_INSTALL_TIMEOUT_MS,
});
}

async function buildProject(projectPath: string) {
return execa('npm', ['run', 'build'], {
cwd: getCommandCwd(projectPath),
timeout: NPM_BUILD_TIMEOUT_MS,
});
}

function getCommandCwd(projectPath: string): string {
try {
return realpathSync(projectPath);
} catch {
return projectPath;
}
}

describe('Build Verification Tests', () => {
const projectPaths: string[] = [];

afterEach(() => {
projectPaths.forEach(cleanupProject);
projectPaths.length = 0;
});
}, TEST_HOOK_TIMEOUT_MS);

describe('Next.js Projects', () => {
it('should generate and build a minimal Next.js project', async () => {
Expand All @@ -70,23 +102,17 @@ describe('Build Verification Tests', () => {

// Install dependencies
console.log('Installing dependencies for Next.js project...');
const installResult = await execa('npm', ['install'], {
cwd: config.path,
timeout: 120000, // 2 minutes
});
const installResult = await installDependencies(config.path);
expect(installResult.exitCode).toBe(0);

// Build the project
console.log('Building Next.js project...');
const buildResult = await execa('npm', ['run', 'build'], {
cwd: config.path,
timeout: 180000, // 3 minutes
});
const buildResult = await buildProject(config.path);
expect(buildResult.exitCode).toBe(0);

// Verify build output exists
expect(existsSync(join(config.path, '.next'))).toBe(true);
}, 360000); // 6 minute timeout for entire test
}, 720000); // 12 minute timeout for entire test

it('should generate and build Next.js with Tailwind', async () => {
const config = createConfig('nextjs-build-tailwind', {
Expand All @@ -102,22 +128,16 @@ describe('Build Verification Tests', () => {

// Install dependencies
console.log('Installing dependencies for Next.js + Tailwind project...');
const installResult = await execa('npm', ['install'], {
cwd: config.path,
timeout: 120000,
});
const installResult = await installDependencies(config.path);
expect(installResult.exitCode).toBe(0);

// Build the project
console.log('Building Next.js + Tailwind project...');
const buildResult = await execa('npm', ['run', 'build'], {
cwd: config.path,
timeout: 180000,
});
const buildResult = await buildProject(config.path);
expect(buildResult.exitCode).toBe(0);

expect(existsSync(join(config.path, '.next'))).toBe(true);
}, 360000);
}, 720000);

it('should generate and build Next.js with state management', async () => {
const config = createConfig('nextjs-build-zustand', {
Expand All @@ -134,22 +154,16 @@ describe('Build Verification Tests', () => {

// Install dependencies
console.log('Installing dependencies for Next.js + Zustand project...');
const installResult = await execa('npm', ['install'], {
cwd: config.path,
timeout: 120000,
});
const installResult = await installDependencies(config.path);
expect(installResult.exitCode).toBe(0);

// Build the project
console.log('Building Next.js + Zustand project...');
const buildResult = await execa('npm', ['run', 'build'], {
cwd: config.path,
timeout: 180000,
});
const buildResult = await buildProject(config.path);
expect(buildResult.exitCode).toBe(0);

expect(existsSync(join(config.path, '.next'))).toBe(true);
}, 360000);
}, 720000);
});

describe('Vite Projects', () => {
Expand All @@ -168,23 +182,17 @@ describe('Build Verification Tests', () => {

// Install dependencies
console.log('Installing dependencies for Vite project...');
const installResult = await execa('npm', ['install'], {
cwd: config.path,
timeout: 120000,
});
const installResult = await installDependencies(config.path);
expect(installResult.exitCode).toBe(0);

// Build the project
console.log('Building Vite project...');
const buildResult = await execa('npm', ['run', 'build'], {
cwd: config.path,
timeout: 120000,
});
const buildResult = await buildProject(config.path);
expect(buildResult.exitCode).toBe(0);

// Verify build output exists
expect(existsSync(join(config.path, 'dist'))).toBe(true);
}, 300000); // 5 minute timeout
}, 600000); // 10 minute timeout

it('should generate and build Vite with Tailwind', async () => {
const config = createConfig('vite-build-tailwind', {
Expand All @@ -200,22 +208,16 @@ describe('Build Verification Tests', () => {

// Install dependencies
console.log('Installing dependencies for Vite + Tailwind project...');
const installResult = await execa('npm', ['install'], {
cwd: config.path,
timeout: 120000,
});
const installResult = await installDependencies(config.path);
expect(installResult.exitCode).toBe(0);

// Build the project
console.log('Building Vite + Tailwind project...');
const buildResult = await execa('npm', ['run', 'build'], {
cwd: config.path,
timeout: 120000,
});
const buildResult = await buildProject(config.path);
expect(buildResult.exitCode).toBe(0);

expect(existsSync(join(config.path, 'dist'))).toBe(true);
}, 300000);
}, 600000);

it('should generate and build Vite with full stack (Tailwind + Zustand + TanStack Query)', async () => {
const config = createConfig('vite-build-full', {
Expand All @@ -233,22 +235,15 @@ describe('Build Verification Tests', () => {

// Install dependencies
console.log('Installing dependencies for Vite full stack project...');
const installResult = await execa('npm', ['install'], {
cwd: config.path,
timeout: 120000,
});
const installResult = await installDependencies(config.path);
expect(installResult.exitCode).toBe(0);

// Build the project
console.log('Building Vite full stack project...');
const buildResult = await execa('npm', ['run', 'build'], {
cwd: config.path,
timeout: 120000,
});
const buildResult = await buildProject(config.path);
expect(buildResult.exitCode).toBe(0);

expect(existsSync(join(config.path, 'dist'))).toBe(true);
}, 300000);
}, 600000);
});
});

Loading
Loading