feature flags
This commit is contained in:
110
apps/api/src/config/feature-loader.test.ts
Normal file
110
apps/api/src/config/feature-loader.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Test file for the new feature flag configuration system
|
||||
*
|
||||
* Run with: npm test -- feature-loader.test.ts
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
||||
import { loadFeatureConfig, isFeatureEnabled, getFeatureState, getAllFeatures } from './feature-loader';
|
||||
import { FlattenedFeatures, FeatureState } from './feature-types';
|
||||
|
||||
describe('Feature Flag Configuration', () => {
|
||||
const originalEnv = process.env.NODE_ENV;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset environment if needed
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Restore original environment
|
||||
process.env.NODE_ENV = originalEnv;
|
||||
});
|
||||
|
||||
describe('loadFeatureConfig', () => {
|
||||
it('should load development config when NODE_ENV=development', async () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
const result = await loadFeatureConfig();
|
||||
|
||||
expect(result.loadedFrom).toBe('config-file');
|
||||
expect(result.features).toBeDefined();
|
||||
expect(result.features['sponsors.portal']).toBe('enabled');
|
||||
expect(result.features['admin.dashboard']).toBe('enabled');
|
||||
});
|
||||
|
||||
it('should load test config when NODE_ENV=test', async () => {
|
||||
process.env.NODE_ENV = 'test';
|
||||
const result = await loadFeatureConfig();
|
||||
|
||||
expect(result.loadedFrom).toBe('config-file');
|
||||
expect(result.features['sponsors.portal']).toBe('enabled');
|
||||
expect(result.features['admin.dashboard']).toBe('enabled');
|
||||
});
|
||||
|
||||
it('should load production config when NODE_ENV=production', async () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
const result = await loadFeatureConfig();
|
||||
|
||||
expect(result.loadedFrom).toBe('config-file');
|
||||
expect(result.features['sponsors.portal']).toBe('enabled');
|
||||
expect(result.features['sponsors.management']).toBe('disabled');
|
||||
});
|
||||
|
||||
|
||||
it('should handle invalid environment', async () => {
|
||||
process.env.NODE_ENV = 'invalid-env';
|
||||
|
||||
await expect(loadFeatureConfig()).rejects.toThrow('Invalid environment');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFeatureEnabled', () => {
|
||||
it('should return true for enabled features', () => {
|
||||
const features: FlattenedFeatures = { 'sponsors.portal': 'enabled' };
|
||||
expect(isFeatureEnabled(features, 'sponsors.portal')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for disabled features', () => {
|
||||
const features: FlattenedFeatures = { 'sponsors.portal': 'disabled' };
|
||||
expect(isFeatureEnabled(features, 'sponsors.portal')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for non-existent features', () => {
|
||||
const features: FlattenedFeatures = { 'sponsors.portal': 'enabled' };
|
||||
expect(isFeatureEnabled(features, 'nonexistent.feature')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFeatureState', () => {
|
||||
it('should return correct state for existing features', () => {
|
||||
const features: FlattenedFeatures = { 'sponsors.portal': 'coming_soon' };
|
||||
expect(getFeatureState(features, 'sponsors.portal')).toBe('coming_soon');
|
||||
});
|
||||
|
||||
it('should return disabled for non-existent features', () => {
|
||||
const features: FlattenedFeatures = { 'sponsors.portal': 'enabled' };
|
||||
expect(getFeatureState(features, 'nonexistent')).toBe('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllFeatures', () => {
|
||||
it('should return all features for current environment', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
const features = getAllFeatures();
|
||||
|
||||
expect(features).toBeDefined();
|
||||
expect(Object.keys(features).length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Flattening behavior', () => {
|
||||
it('should flatten nested feature configurations', async () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
const result = await loadFeatureConfig();
|
||||
|
||||
// Should flatten nested objects
|
||||
expect(result.features['sponsors.portal']).toBeDefined();
|
||||
expect(result.features['sponsors.dashboard']).toBeDefined();
|
||||
expect(result.features['admin.dashboard']).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
129
apps/api/src/config/feature-loader.ts
Normal file
129
apps/api/src/config/feature-loader.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { featureConfig } from './features.config';
|
||||
import {
|
||||
FeatureFlagConfig,
|
||||
FlattenedFeatures,
|
||||
ConfigLoadResult,
|
||||
FeatureState
|
||||
} from './feature-types';
|
||||
|
||||
/**
|
||||
* Default configuration path relative to project root
|
||||
*/
|
||||
const DEFAULT_CONFIG_PATH = 'apps/api/src/config/features.config.ts';
|
||||
|
||||
/**
|
||||
* Flattens nested feature configuration into a simple key-value map
|
||||
*
|
||||
* Example:
|
||||
* Input: { sponsors: { portal: 'enabled' } }
|
||||
* Output: { 'sponsors.portal': 'enabled' }
|
||||
*/
|
||||
function flattenFeatures(
|
||||
config: Record<string, any>,
|
||||
prefix: string = ''
|
||||
): FlattenedFeatures {
|
||||
const flattened: FlattenedFeatures = {};
|
||||
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
// Recursively flatten nested objects
|
||||
Object.assign(flattened, flattenFeatures(value, fullKey));
|
||||
} else if (isFeatureState(value)) {
|
||||
// Assign feature state
|
||||
flattened[fullKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return flattened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for FeatureState
|
||||
*/
|
||||
function isFeatureState(value: unknown): value is FeatureState {
|
||||
return (
|
||||
value === 'enabled' ||
|
||||
value === 'disabled' ||
|
||||
value === 'coming_soon' ||
|
||||
value === 'hidden'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current environment
|
||||
*/
|
||||
function getEnvironment(): string {
|
||||
return process.env.NODE_ENV || 'development';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the environment exists in the config
|
||||
*/
|
||||
function validateEnvironment(
|
||||
env: string
|
||||
): env is keyof FeatureFlagConfig {
|
||||
const validEnvs = ['development', 'test', 'staging', 'production'];
|
||||
if (!validEnvs.includes(env)) {
|
||||
throw new Error(
|
||||
`Invalid environment: "${env}". Valid environments: ${validEnvs.join(', ')}`
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads feature configuration from the TypeScript config file
|
||||
*/
|
||||
export async function loadFeatureConfig(): Promise<ConfigLoadResult> {
|
||||
const env = getEnvironment();
|
||||
|
||||
if (validateEnvironment(env)) {
|
||||
const envConfig = featureConfig[env];
|
||||
const flattened = flattenFeatures(envConfig);
|
||||
|
||||
return {
|
||||
features: flattened,
|
||||
loadedFrom: 'config-file',
|
||||
configPath: DEFAULT_CONFIG_PATH,
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback to defaults
|
||||
return {
|
||||
features: {},
|
||||
loadedFrom: 'defaults',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific feature state
|
||||
*/
|
||||
export function getFeatureState(
|
||||
features: FlattenedFeatures,
|
||||
featurePath: string
|
||||
): FeatureState {
|
||||
return features[featurePath] || 'disabled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a feature is enabled
|
||||
*/
|
||||
export function isFeatureEnabled(
|
||||
features: FlattenedFeatures,
|
||||
featurePath: string
|
||||
): boolean {
|
||||
return getFeatureState(features, featurePath) === 'enabled';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all features for debugging
|
||||
*/
|
||||
export function getAllFeatures(): FlattenedFeatures {
|
||||
const env = getEnvironment();
|
||||
if (validateEnvironment(env)) {
|
||||
return flattenFeatures(featureConfig[env]);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
53
apps/api/src/config/feature-types.ts
Normal file
53
apps/api/src/config/feature-types.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Feature Flag Configuration Types
|
||||
*
|
||||
* Provides type-safe configuration for feature flags across different environments
|
||||
*/
|
||||
|
||||
export type FeatureState = 'enabled' | 'disabled' | 'coming_soon' | 'hidden';
|
||||
|
||||
/**
|
||||
* Individual feature configuration
|
||||
*/
|
||||
export interface FeatureConfig {
|
||||
[featureName: string]: FeatureState | FeatureConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Environment-specific feature configurations
|
||||
*/
|
||||
export interface EnvironmentConfig {
|
||||
[featureGroup: string]: FeatureConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete feature flag configuration
|
||||
*/
|
||||
export interface FeatureFlagConfig {
|
||||
development: EnvironmentConfig;
|
||||
test: EnvironmentConfig;
|
||||
staging: EnvironmentConfig;
|
||||
production: EnvironmentConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattened feature map for runtime lookup
|
||||
* Example: { 'sponsors.portal': 'enabled', 'admin.dashboard': 'enabled' }
|
||||
*/
|
||||
export type FlattenedFeatures = Record<string, FeatureState>;
|
||||
|
||||
/**
|
||||
* Configuration loader result
|
||||
*/
|
||||
export interface ConfigLoadResult {
|
||||
features: FlattenedFeatures;
|
||||
loadedFrom: 'config-file' | 'env-var' | 'defaults';
|
||||
configPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy JSON format for backward compatibility
|
||||
*/
|
||||
export interface LegacyFeatureConfig {
|
||||
[key: string]: FeatureState | LegacyFeatureConfig;
|
||||
}
|
||||
82
apps/api/src/config/features.config.example.ts
Normal file
82
apps/api/src/config/features.config.example.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { FeatureFlagConfig } from './feature-types';
|
||||
|
||||
/**
|
||||
* Example feature flag configuration
|
||||
*
|
||||
* Copy this file to features.config.ts and customize for your needs
|
||||
*
|
||||
* Feature states:
|
||||
* - 'enabled': Feature is fully available
|
||||
* - 'disabled': Feature is turned off
|
||||
* - 'coming_soon': Feature is in development, shown as "coming soon"
|
||||
* - 'hidden': Feature is hidden from users
|
||||
*/
|
||||
export const featureConfig: FeatureFlagConfig = {
|
||||
development: {
|
||||
sponsors: {
|
||||
portal: 'enabled',
|
||||
dashboard: 'enabled',
|
||||
management: 'enabled',
|
||||
},
|
||||
admin: {
|
||||
dashboard: 'enabled',
|
||||
userManagement: 'enabled',
|
||||
analytics: 'enabled',
|
||||
},
|
||||
beta: {
|
||||
newUI: 'enabled',
|
||||
experimental: 'coming_soon',
|
||||
},
|
||||
},
|
||||
|
||||
test: {
|
||||
sponsors: {
|
||||
portal: 'enabled',
|
||||
dashboard: 'enabled',
|
||||
management: 'enabled',
|
||||
},
|
||||
admin: {
|
||||
dashboard: 'enabled',
|
||||
userManagement: 'enabled',
|
||||
analytics: 'enabled',
|
||||
},
|
||||
beta: {
|
||||
newUI: 'disabled',
|
||||
experimental: 'disabled',
|
||||
},
|
||||
},
|
||||
|
||||
staging: {
|
||||
sponsors: {
|
||||
portal: 'enabled',
|
||||
dashboard: 'enabled',
|
||||
management: 'enabled',
|
||||
},
|
||||
admin: {
|
||||
dashboard: 'enabled',
|
||||
userManagement: 'enabled',
|
||||
analytics: 'enabled',
|
||||
},
|
||||
beta: {
|
||||
newUI: 'coming_soon',
|
||||
experimental: 'hidden',
|
||||
},
|
||||
},
|
||||
|
||||
production: {
|
||||
sponsors: {
|
||||
portal: 'enabled',
|
||||
dashboard: 'enabled',
|
||||
management: 'disabled',
|
||||
},
|
||||
admin: {
|
||||
dashboard: 'enabled',
|
||||
userManagement: 'enabled',
|
||||
analytics: 'disabled',
|
||||
},
|
||||
beta: {
|
||||
newUI: 'disabled',
|
||||
experimental: 'hidden',
|
||||
},
|
||||
},
|
||||
};
|
||||
79
apps/api/src/config/features.config.ts
Normal file
79
apps/api/src/config/features.config.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { FeatureFlagConfig } from './feature-types';
|
||||
|
||||
/**
|
||||
* Feature flag configuration for all environments
|
||||
* This provides type safety, IntelliSense, and environment-specific settings
|
||||
*/
|
||||
export const featureConfig: FeatureFlagConfig = {
|
||||
// Development environment - features for local development
|
||||
development: {
|
||||
sponsors: {
|
||||
portal: 'enabled',
|
||||
dashboard: 'enabled',
|
||||
management: 'enabled',
|
||||
},
|
||||
admin: {
|
||||
dashboard: 'enabled',
|
||||
userManagement: 'enabled',
|
||||
analytics: 'enabled',
|
||||
},
|
||||
beta: {
|
||||
newUI: 'enabled', // Enable new UI for testing
|
||||
experimental: 'coming_soon',
|
||||
},
|
||||
},
|
||||
|
||||
// Test environment - features for automated tests
|
||||
test: {
|
||||
sponsors: {
|
||||
portal: 'enabled',
|
||||
dashboard: 'enabled',
|
||||
management: 'enabled',
|
||||
},
|
||||
admin: {
|
||||
dashboard: 'enabled',
|
||||
userManagement: 'enabled',
|
||||
analytics: 'enabled',
|
||||
},
|
||||
beta: {
|
||||
newUI: 'disabled',
|
||||
experimental: 'disabled',
|
||||
},
|
||||
},
|
||||
|
||||
// Staging environment - features for pre-production testing
|
||||
staging: {
|
||||
sponsors: {
|
||||
portal: 'enabled',
|
||||
dashboard: 'enabled',
|
||||
management: 'enabled',
|
||||
},
|
||||
admin: {
|
||||
dashboard: 'enabled',
|
||||
userManagement: 'enabled',
|
||||
analytics: 'enabled',
|
||||
},
|
||||
beta: {
|
||||
newUI: 'coming_soon', // Ready for testing but not fully rolled out
|
||||
experimental: 'hidden',
|
||||
},
|
||||
},
|
||||
|
||||
// Production environment - stable features only
|
||||
production: {
|
||||
sponsors: {
|
||||
portal: 'enabled',
|
||||
dashboard: 'enabled',
|
||||
management: 'disabled', // Feature not ready yet
|
||||
},
|
||||
admin: {
|
||||
dashboard: 'enabled',
|
||||
userManagement: 'enabled',
|
||||
analytics: 'disabled', // Feature not ready yet
|
||||
},
|
||||
beta: {
|
||||
newUI: 'disabled',
|
||||
experimental: 'hidden',
|
||||
},
|
||||
},
|
||||
};
|
||||
43
apps/api/src/config/integration.test.ts
Normal file
43
apps/api/src/config/integration.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Integration test to verify the feature flag system works end-to-end
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { loadFeatureConfig, isFeatureEnabled, getFeatureState } from './feature-loader';
|
||||
|
||||
describe('Feature Flag Integration Test', () => {
|
||||
it('should load config and provide correct feature states', async () => {
|
||||
// Set test environment
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
const result = await loadFeatureConfig();
|
||||
|
||||
// Verify config loaded from file
|
||||
expect(result.loadedFrom).toBe('config-file');
|
||||
expect(result.configPath).toBe('apps/api/src/config/features.config.ts');
|
||||
|
||||
// Verify specific features from our config
|
||||
expect(result.features['sponsors.portal']).toBe('enabled');
|
||||
expect(result.features['sponsors.dashboard']).toBe('enabled');
|
||||
expect(result.features['admin.dashboard']).toBe('enabled');
|
||||
expect(result.features['beta.newUI']).toBe('disabled');
|
||||
|
||||
// Verify utility functions work
|
||||
expect(isFeatureEnabled(result.features, 'sponsors.portal')).toBe(true);
|
||||
expect(isFeatureEnabled(result.features, 'beta.newUI')).toBe(false);
|
||||
expect(getFeatureState(result.features, 'sponsors.portal')).toBe('enabled');
|
||||
expect(getFeatureState(result.features, 'nonexistent')).toBe('disabled');
|
||||
});
|
||||
|
||||
it('should work with different environments', async () => {
|
||||
// Test development environment
|
||||
process.env.NODE_ENV = 'development';
|
||||
const devResult = await loadFeatureConfig();
|
||||
expect(devResult.features['beta.newUI']).toBe('enabled'); // dev has beta enabled
|
||||
|
||||
// Test production environment
|
||||
process.env.NODE_ENV = 'production';
|
||||
const prodResult = await loadFeatureConfig();
|
||||
expect(prodResult.features['beta.newUI']).toBe('disabled'); // prod has beta disabled
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { loadFeatureConfig } from '../../config/feature-loader';
|
||||
import { FlattenedFeatures } from '../../config/feature-types';
|
||||
|
||||
export type OperationalMode = 'normal' | 'maintenance' | 'test';
|
||||
export type FeatureState = 'enabled' | 'disabled' | 'coming_soon' | 'hidden';
|
||||
@@ -72,24 +74,18 @@ export class PolicyService {
|
||||
};
|
||||
}
|
||||
|
||||
const anyEnvConfigured =
|
||||
Boolean(process.env.GRIDPILOT_OPERATIONAL_MODE) ||
|
||||
Boolean(process.env.GRIDPILOT_FEATURES_JSON) ||
|
||||
Boolean(process.env.GRIDPILOT_MAINTENANCE_ALLOW_VIEW) ||
|
||||
Boolean(process.env.GRIDPILOT_MAINTENANCE_ALLOW_MUTATE);
|
||||
|
||||
const raw: RawPolicySnapshot = {};
|
||||
// Load from TypeScript config file
|
||||
const configResult = await loadFeatureConfig();
|
||||
const raw: RawPolicySnapshot = {
|
||||
capabilities: convertFlattenedToCapabilities(configResult.features),
|
||||
};
|
||||
|
||||
// Include other env vars if set
|
||||
const operationalMode = process.env.GRIDPILOT_OPERATIONAL_MODE;
|
||||
if (operationalMode) {
|
||||
raw.operationalMode = operationalMode;
|
||||
}
|
||||
|
||||
const capabilities = parseFeaturesJson(process.env.GRIDPILOT_FEATURES_JSON);
|
||||
if (capabilities) {
|
||||
raw.capabilities = capabilities;
|
||||
}
|
||||
|
||||
const maintenanceAllowView = parseCsvList(process.env.GRIDPILOT_MAINTENANCE_ALLOW_VIEW);
|
||||
const maintenanceAllowMutate = parseCsvList(process.env.GRIDPILOT_MAINTENANCE_ALLOW_MUTATE);
|
||||
|
||||
@@ -102,7 +98,7 @@ export class PolicyService {
|
||||
|
||||
return {
|
||||
raw,
|
||||
loadedFrom: anyEnvConfigured ? 'env' : 'defaults',
|
||||
loadedFrom: configResult.loadedFrom === 'config-file' ? 'file' : 'defaults',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -191,16 +187,6 @@ function parseFeatureState(value: unknown): FeatureState | null {
|
||||
}
|
||||
}
|
||||
|
||||
function parseFeaturesJson(raw: string | undefined): unknown {
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw) as unknown;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function parseCsvList(raw: string | undefined): readonly string[] | undefined {
|
||||
if (!raw) {
|
||||
@@ -226,4 +212,11 @@ function normalizeStringArray(value: unknown): readonly string[] {
|
||||
.filter(Boolean);
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// NEW: Helper function to convert flattened features to capabilities
|
||||
function convertFlattenedToCapabilities(
|
||||
flattened: FlattenedFeatures
|
||||
): Record<string, FeatureState> {
|
||||
return flattened as Record<string, FeatureState>;
|
||||
}
|
||||
1
apps/api/src/env.d.ts
vendored
1
apps/api/src/env.d.ts
vendored
@@ -20,7 +20,6 @@ declare global {
|
||||
GRIDPILOT_POLICY_CACHE_MS?: string;
|
||||
GRIDPILOT_POLICY_PATH?: string;
|
||||
GRIDPILOT_OPERATIONAL_MODE?: string;
|
||||
GRIDPILOT_FEATURES_JSON?: string;
|
||||
GRIDPILOT_MAINTENANCE_ALLOW_VIEW?: string;
|
||||
GRIDPILOT_MAINTENANCE_ALLOW_MUTATE?: string;
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ services:
|
||||
- GRIDPILOT_API_PERSISTENCE=postgres
|
||||
- GRIDPILOT_API_BOOTSTRAP=true
|
||||
- GRIDPILOT_API_FORCE_RESEED=true
|
||||
- GRIDPILOT_FEATURES_JSON={"sponsors.portal":"enabled","admin.dashboard":"enabled"}
|
||||
- DATABASE_URL=postgres://gridpilot_e2e_user:gridpilot_e2e_pass@db:5432/gridpilot_e2e
|
||||
- POSTGRES_DB=gridpilot_e2e
|
||||
- POSTGRES_USER=gridpilot_e2e_user
|
||||
|
||||
@@ -46,7 +46,6 @@ services:
|
||||
- GRIDPILOT_API_PERSISTENCE=postgres
|
||||
- GRIDPILOT_API_BOOTSTRAP=true
|
||||
- GRIDPILOT_API_FORCE_RESEED=true
|
||||
- GRIDPILOT_FEATURES_JSON={"sponsors.portal":"enabled","admin.dashboard":"enabled"}
|
||||
- DATABASE_URL=postgres://gridpilot_test_user:gridpilot_test_pass@db:5432/gridpilot_test
|
||||
- POSTGRES_DB=gridpilot_test
|
||||
- POSTGRES_USER=gridpilot_test_user
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { WebsiteRouteManager } from '../../shared/website/WebsiteRouteManager';
|
||||
import { WebsiteAuthManager } from '../../shared/website/WebsiteAuthManager';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { ConsoleErrorCapture } from '../../shared/website/ConsoleErrorCapture';
|
||||
import { WebsiteAuthManager } from '../../shared/website/WebsiteAuthManager';
|
||||
import { WebsiteRouteManager } from '../../shared/website/WebsiteRouteManager';
|
||||
|
||||
const WEBSITE_BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000';
|
||||
|
||||
@@ -538,4 +538,5 @@ test.describe('Website Pages - TypeORM Integration', () => {
|
||||
expect(detailBodyText).toBeTruthy();
|
||||
expect(detailBodyText?.length).toBeGreaterThan(50);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user