Improve desktop runtime execution tests

Test improvements:

- Capture titles for all macOS windows, not just the frontmost.
- Incorporate missing application log files.
- Improve log clarity with enriched context.
- Improve application termination on macOS by reducing grace period.
- Ensure complete application termination on macOS.
- Validate Vue application loading through an initial log.
- Support ignoring environment-specific `stderr` errors.
- Do not fail the test if working directory cannot be deleted.
- Use retry pattern when installing dependencies due to network errors.

Refactorings:

- Migrate the test code to TypeScript.
- Replace deprecated `rmdir` with `rm` for error-resistant directory
  removal.
- Improve sanity checking by shifting from App.vue to Vue bootstrapper.
- Centralize environment variable management with `EnvironmentVariables`
  construct.
- Rename infrastructure/Environment to RuntimeEnvironment for clarity.
- Isolate WindowVariables and SystemOperations from RuntimeEnvironment.
- Inject logging via preloader.
- Correct mislabeled RuntimeSanity tests.

Configuration:

- Introduce `npm run check:desktop` for simplified execution.
- Omit `console.log` override due to `nodeIntegration` restrictions and
  reveal logging functionality using context-bridging.
This commit is contained in:
undergroundwires
2023-08-29 16:30:00 +02:00
parent 35be05df20
commit ad0576a752
146 changed files with 2418 additions and 1186 deletions

View File

@@ -0,0 +1,15 @@
import { describe, it } from 'vitest';
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
describe('EnvironmentVariablesFactory', () => {
it('can read current environment', () => {
const environmentVariables = EnvironmentVariablesFactory.Current.instance;
Object.entries(environmentVariables).forEach(([key, value]) => {
it(`${key} value is defined`, () => {
expect(value).to.not.equal(undefined);
expect(value).to.not.equal(null);
expect(value).to.not.equal(Number.NaN);
});
});
});
});

View File

@@ -1,15 +1,16 @@
import { describe, it, expect } from 'vitest';
import { ViteAppMetadata } from '@/infrastructure/Metadata/Vite/ViteAppMetadata';
import packageJson from '@/../package.json' assert { type: 'json' };
import { PropertyKeys } from '@/TypeHelpers';
import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
import { ViteEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables';
describe('ViteAppMetadata', () => {
describe('populates from package.json', () => {
describe('ViteEnvironmentVariables', () => {
describe('populates metadata from package.json', () => {
interface ITestCase {
readonly getActualValue: (sut: ViteAppMetadata) => string;
readonly getActualValue: (sut: IAppMetadata) => string;
readonly expected: string;
}
const testCases: { readonly [K in PropertyKeys<ViteAppMetadata>]: ITestCase } = {
const testCases: { readonly [K in PropertyKeys<IAppMetadata>]: ITestCase } = {
name: {
expected: packageJson.name,
getActualValue: (sut) => sut.name,
@@ -34,7 +35,7 @@ describe('ViteAppMetadata', () => {
Object.entries(testCases).forEach(([propertyName, { expected, getActualValue }]) => {
it(`should correctly get the value of ${propertyName}`, () => {
// arrange
const sut = new ViteAppMetadata();
const sut = new ViteEnvironmentVariables();
// act
const actualValue = getActualValue(sut);

View File

@@ -22,8 +22,8 @@ describe('SanityChecks', () => {
function generateTestOptions(): ISanityCheckOptions[] {
const defaultOptions: ISanityCheckOptions = {
validateMetadata: true,
validateEnvironment: true,
validateEnvironmentVariables: true,
validateWindowVariables: true,
};
return generateBooleanPermutations(defaultOptions);
}

View File

@@ -1,15 +0,0 @@
import { describe } from 'vitest';
import { EnvironmentValidator } from '@/infrastructure/RuntimeSanity/Validators/EnvironmentValidator';
import { FactoryFunction } from '@/infrastructure/RuntimeSanity/Common/FactoryValidator';
import { EnvironmentStub } from '@tests/unit/shared/Stubs/EnvironmentStub';
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
import { runFactoryValidatorTests } from './FactoryValidatorConcreteTestRunner';
describe('EnvironmentValidator', () => {
runFactoryValidatorTests({
createValidator: (factory?: FactoryFunction<IEnvironment>) => new EnvironmentValidator(factory),
enablingOptionProperty: 'validateEnvironment',
factoryFunctionStub: () => new EnvironmentStub(),
expectedValidatorName: 'environment',
});
});

View File

@@ -0,0 +1,7 @@
import { describe } from 'vitest';
import { EnvironmentVariablesValidator } from '@/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator';
import { itNoErrorsOnCurrentEnvironment } from './ValidatorTestRunner';
describe('EnvironmentVariablesValidator', () => {
itNoErrorsOnCurrentEnvironment(() => new EnvironmentVariablesValidator());
});

View File

@@ -1,15 +0,0 @@
import { describe } from 'vitest';
import { MetadataValidator } from '@/infrastructure/RuntimeSanity/Validators/MetadataValidator';
import { FactoryFunction } from '@/infrastructure/RuntimeSanity/Common/FactoryValidator';
import { AppMetadataStub } from '@tests/unit/shared/Stubs/AppMetadataStub';
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
import { runFactoryValidatorTests } from './FactoryValidatorConcreteTestRunner';
describe('MetadataValidator', () => {
runFactoryValidatorTests({
createValidator: (factory?: FactoryFunction<IAppMetadata>) => new MetadataValidator(factory),
enablingOptionProperty: 'validateMetadata',
factoryFunctionStub: () => new AppMetadataStub(),
expectedValidatorName: 'metadata',
});
});

View File

@@ -0,0 +1,7 @@
import { describe } from 'vitest';
import { WindowVariablesValidator } from '@/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator';
import { itNoErrorsOnCurrentEnvironment } from './ValidatorTestRunner';
describe('WindowVariablesValidator', () => {
itNoErrorsOnCurrentEnvironment(() => new WindowVariablesValidator());
});

View File

@@ -0,0 +1,35 @@
import { it, describe, expect } from 'vitest';
import { provideWindowVariables } from '@/presentation/electron/preload/WindowVariablesProvider';
describe('WindowVariablesProvider', () => {
describe('provideWindowVariables', () => {
describe('conforms to Electron\'s context bridging requirements', () => {
// https://www.electronjs.org/docs/latest/api/context-bridge
const variables = provideWindowVariables();
Object.entries(variables).forEach(([key, value]) => {
it(`\`${key}\` conforms to allowed types for context bridging`, () => {
expect(checkAllowedType(value)).to.equal(true);
});
});
});
});
});
function checkAllowedType(value: unknown) {
const type = typeof value;
if (['string', 'number', 'boolean', 'function'].includes(type)) {
return true;
}
if (Array.isArray(value)) {
return value.every(checkAllowedType);
}
if (type === 'object' && value !== null) {
return (
// Every key should be a string
Object.keys(value).every((key) => typeof key === 'string')
// Every value should be of allowed type
&& Object.values(value).every(checkAllowedType)
);
}
return false;
}

View File

@@ -4,7 +4,7 @@ import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { buildContext } from '@/application/Context/ApplicationContextFactory';
import { IApplicationFactory } from '@/application/IApplicationFactory';
import { IApplication } from '@/domain/IApplication';
import { EnvironmentStub } from '@tests/unit/shared/Stubs/EnvironmentStub';
import { RuntimeEnvironmentStub } from '@tests/unit/shared/Stubs/RuntimeEnvironmentStub';
import { ApplicationStub } from '@tests/unit/shared/Stubs/ApplicationStub';
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
import { expectThrowsAsync } from '@tests/unit/shared/Assertions/ExpectThrowsAsync';
@@ -38,7 +38,7 @@ describe('ApplicationContextFactory', () => {
it('returns currentOs if it is supported', async () => {
// arrange
const expected = OperatingSystem.Windows;
const environment = new EnvironmentStub().withOs(expected);
const environment = new RuntimeEnvironmentStub().withOs(expected);
const collection = new CategoryCollectionStub().withOs(expected);
const factoryMock = mockFactoryWithCollection(collection);
// act
@@ -51,7 +51,7 @@ describe('ApplicationContextFactory', () => {
// arrange
const expected = OperatingSystem.Windows;
const currentOs = OperatingSystem.macOS;
const environment = new EnvironmentStub().withOs(currentOs);
const environment = new RuntimeEnvironmentStub().withOs(currentOs);
const collection = new CategoryCollectionStub().withOs(expected);
const factoryMock = mockFactoryWithCollection(collection);
// act
@@ -68,7 +68,7 @@ describe('ApplicationContextFactory', () => {
new CategoryCollectionStub().withOs(expectedOs).withTotalScripts(5),
new CategoryCollectionStub().withOs(OperatingSystem.Windows).withTotalScripts(4),
];
const environment = new EnvironmentStub().withOs(OperatingSystem.macOS);
const environment = new RuntimeEnvironmentStub().withOs(OperatingSystem.macOS);
const app = new ApplicationStub().withCollections(...allCollections);
const factoryMock = mockFactoryWithApp(app);
// act

View File

@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
import type { CollectionData } from '@/application/collections/';
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
import { CategoryCollectionParserType, parseApplication } from '@/application/Parser/ApplicationParser';
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
import WindowsData from '@/application/collections/windows.yaml';
import MacOsData from '@/application/collections/macos.yaml';
import LinuxData from '@/application/collections/linux.yaml';
@@ -11,7 +11,7 @@ import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollect
import { CollectionDataStub } from '@tests/unit/shared/Stubs/CollectionDataStub';
import { getAbsentCollectionTestCases, getAbsentObjectTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import { AppMetadataStub } from '@tests/unit/shared/Stubs/AppMetadataStub';
import { AppMetadataFactory } from '@/infrastructure/Metadata/AppMetadataFactory';
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
import { CategoryCollectionParserStub } from '@tests/unit/shared/Stubs/CategoryCollectionParserStub';
import { ProjectInformationParserStub } from '@tests/unit/shared/Stubs/ProjectInformationParserStub';
import { ProjectInformationStub } from '@tests/unit/shared/Stubs/ProjectInformationStub';
@@ -85,7 +85,7 @@ describe('ApplicationParser', () => {
});
it('defaults to metadata from factory', () => {
// arrange
const expectedMetadata: IAppMetadata = AppMetadataFactory.Current.instance;
const expectedMetadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance;
const infoParserStub = new ProjectInformationParserStub();
// act
new ApplicationParserBuilder()
@@ -204,9 +204,9 @@ class ApplicationParserBuilder {
}
public withMetadata(
environment: IAppMetadata,
metadata: IAppMetadata,
): this {
this.metadata = environment;
this.metadata = metadata;
return this;
}

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { EnvironmentStub } from '@tests/unit/shared/Stubs/EnvironmentStub';
import { RuntimeEnvironmentStub } from '@tests/unit/shared/Stubs/RuntimeEnvironmentStub';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { CodeRunner } from '@/infrastructure/CodeRunner';
import { expectThrowsAsync } from '@tests/unit/shared/Assertions/ExpectThrowsAsync';
@@ -8,23 +8,10 @@ import { OperatingSystemOpsStub } from '@tests/unit/shared/Stubs/OperatingSystem
import { LocationOpsStub } from '@tests/unit/shared/Stubs/LocationOpsStub';
import { FileSystemOpsStub } from '@tests/unit/shared/Stubs/FileSystemOpsStub';
import { CommandOpsStub } from '@tests/unit/shared/Stubs/CommandOpsStub';
import { IFileSystemOps, ISystemOperations } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
import { IFileSystemOps, ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations';
import { FunctionKeys } from '@/TypeHelpers';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('CodeRunner', () => {
describe('ctor throws if system is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing system operations';
const environment = new EnvironmentStub()
.withSystemOperations(absentValue);
// act
const act = () => new CodeRunner(environment);
// assert
expect(act).to.throw(expectedError);
});
});
describe('runCode', () => {
it('creates temporary directory recursively', async () => {
// arrange
@@ -222,10 +209,9 @@ class TestContext {
private systemOperations: ISystemOperations = new SystemOperationsStub();
public async runCode(): Promise<void> {
const environment = new EnvironmentStub()
.withOs(this.os)
.withSystemOperations(this.systemOperations);
const runner = new CodeRunner(environment);
const environment = new RuntimeEnvironmentStub()
.withOs(this.os);
const runner = new CodeRunner(this.systemOperations, environment);
await runner.runCode(this.code, this.folderName, this.fileExtension);
}

View File

@@ -1,160 +0,0 @@
import { describe, it, expect } from 'vitest';
import { validateWindowVariables } from '@/infrastructure/Environment/WindowVariablesValidator';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub';
import { WindowVariables } from '@/infrastructure/Environment/WindowVariables';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('WindowVariablesValidator', () => {
describe('validateWindowVariables', () => {
describe('invalid types', () => {
it('throws an error if variables is not an object', () => {
// arrange
const expectedError = 'window is not an object but string';
const variablesAsString = 'not an object';
// act
const act = () => validateWindowVariables(variablesAsString as unknown);
// assert
expect(act).to.throw(expectedError);
});
it('throws an error if variables is an array', () => {
// arrange
const expectedError = 'window is not an object but object';
const arrayVariables: unknown = [];
// act
const act = () => validateWindowVariables(arrayVariables as unknown);
// assert
expect(act).to.throw(expectedError);
});
describe('throws an error if variables is null', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing variables';
const variables = absentValue;
// act
const act = () => validateWindowVariables(variables as unknown);
// assert
expect(act).to.throw(expectedError);
});
});
});
describe('property validations', () => {
it('throws an error with a description of all invalid properties', () => {
// arrange
const expectedError = 'Unexpected os (string)\nUnexpected isDesktop (string)';
const input = {
os: 'invalid',
isDesktop: 'not a boolean',
};
// act
const act = () => validateWindowVariables(input as unknown);
// assert
expect(act).to.throw(expectedError);
});
describe('`os` property', () => {
it('throws an error when os is not a number', () => {
// arrange
const expectedError = 'Unexpected os (string)';
const input = {
os: 'Linux',
};
// act
const act = () => validateWindowVariables(input as unknown);
// assert
expect(act).to.throw(expectedError);
});
it('throws an error for an invalid numeric os value', () => {
// arrange
const expectedError = 'Unexpected os (number)';
const input = {
os: Number.MAX_SAFE_INTEGER,
};
// act
const act = () => validateWindowVariables(input as unknown);
// assert
expect(act).to.throw(expectedError);
});
it('does not throw for a missing os value', () => {
const input = {
isDesktop: true,
system: new SystemOperationsStub(),
};
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.not.throw();
});
});
describe('`isDesktop` property', () => {
it('throws an error when only isDesktop is provided and it is true without a system object', () => {
// arrange
const expectedError = 'Unexpected system (undefined)';
const input = {
isDesktop: true,
};
// act
const act = () => validateWindowVariables(input as unknown);
// assert
expect(act).to.throw(expectedError);
});
it('does not throw when isDesktop is true with a valid system object', () => {
// arrange
const input = {
isDesktop: true,
system: new SystemOperationsStub(),
};
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.not.throw();
});
it('does not throw when isDesktop is false without a system object', () => {
// arrange
const input = {
isDesktop: false,
};
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.not.throw();
});
});
describe('`system` property', () => {
it('throws an error if system is not an object', () => {
// arrange
const expectedError = 'Unexpected system (string)';
const input = {
isDesktop: true,
system: 'invalid system',
};
// act
const act = () => validateWindowVariables(input as unknown);
// assert
expect(act).to.throw(expectedError);
});
});
});
it('does not throw for a valid object', () => {
const input: WindowVariables = {
os: OperatingSystem.Windows,
isDesktop: true,
system: new SystemOperationsStub(),
};
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.not.throw();
});
});
});

View File

@@ -0,0 +1,53 @@
import {
describe,
} from 'vitest';
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
import { EnvironmentVariablesFactory, EnvironmentVariablesValidator } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
import { ViteEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables';
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
describe('EnvironmentVariablesFactory', () => {
describe('instance', () => {
itIsSingleton({
getter: () => EnvironmentVariablesFactory.Current.instance,
expectedType: ViteEnvironmentVariables,
});
});
it('creates the correct type', () => {
// arrange
const sut = new TestableEnvironmentVariablesFactory();
// act
const metadata = sut.instance;
// assert
expect(metadata).to.be.instanceOf(ViteEnvironmentVariables);
});
it('validates its instance', () => {
// arrange
let validatedInstance: IEnvironmentVariables;
const validatorMock = (instanceToValidate: IEnvironmentVariables) => {
validatedInstance = instanceToValidate;
};
// act
const sut = new TestableEnvironmentVariablesFactory(validatorMock);
const actualInstance = sut.instance;
// assert
expect(actualInstance).to.equal(validatedInstance);
});
it('throws error if validator fails', () => {
// arrange
const expectedError = 'validator failed';
const failingValidator = () => {
throw new Error(expectedError);
};
// act
const act = () => new TestableEnvironmentVariablesFactory(failingValidator);
// assert
expect(act).to.throw(expectedError);
});
});
class TestableEnvironmentVariablesFactory extends EnvironmentVariablesFactory {
public constructor(validator: EnvironmentVariablesValidator = () => { /* NO OP */ }) {
super(validator);
}
}

View File

@@ -0,0 +1,88 @@
import { describe, it, expect } from 'vitest';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { EnvironmentVariablesStub } from '@tests/unit/shared/Stubs/EnvironmentVariablesStub';
import { validateEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator';
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
describe('EnvironmentVariablesValidator', () => {
it('does not throw if all environment keys have values', () => {
// arrange
const environment = new EnvironmentVariablesStub();
// act
const act = () => validateEnvironmentVariables(environment);
// assert
expect(act).to.not.throw();
});
it('does not throw if a boolean key has false value', () => {
// arrange
const environmentWithFalseBoolean: Partial<IEnvironmentVariables> = {
isNonProduction: false,
};
const environment: IEnvironmentVariables = {
...new EnvironmentVariablesStub(),
...environmentWithFalseBoolean,
};
// act
const act = () => validateEnvironmentVariables(environment);
// assert
expect(act).to.not.throw();
});
describe('throws as expected', () => {
describe('"missing environment" if environment is not provided', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing environment';
const environment = absentValue;
// act
const act = () => validateEnvironmentVariables(environment);
// assert
expect(act).to.throw(expectedError);
});
});
it('"missing keys" if environment has properties with missing values', () => {
// arrange
const expectedError = 'Environment keys missing: name, homepageUrl';
const missingData: Partial<IEnvironmentVariables> = {
name: undefined,
homepageUrl: undefined,
};
const environment: IEnvironmentVariables = {
...new EnvironmentVariablesStub(),
...missingData,
};
// act
const act = () => validateEnvironmentVariables(environment);
// assert
expect(act).to.throw(expectedError);
});
it('"missing keys" if environment has getters with missing values', () => {
// arrange
const expectedError = 'Environment keys missing: name, homepageUrl';
const stubWithGetters: Partial<IEnvironmentVariables> = {
get name() {
return undefined;
},
get homepageUrl() {
return undefined;
},
};
const environment: IEnvironmentVariables = {
...new EnvironmentVariablesStub(),
...stubWithGetters,
};
// act
const act = () => validateEnvironmentVariables(environment);
// assert
expect(act).to.throw(expectedError);
});
it('throws "unable to capture" if environment has no getters or properties', () => {
// arrange
const expectedError = 'Unable to capture key/value pairs';
const environment = {} as IEnvironmentVariables;
// act
const act = () => validateEnvironmentVariables(environment);
// assert
expect(act).to.throw(expectedError);
});
});
});

View File

@@ -1,5 +1,5 @@
import { expect, describe, it } from 'vitest';
import { VITE_ENVIRONMENT_KEYS } from '@/infrastructure/Metadata/Vite/ViteEnvironmentKeys';
import { VITE_ENVIRONMENT_KEYS } from '@/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentKeys';
describe('VITE_ENVIRONMENT_KEYS', () => {
describe('each key should have a non-empty string', () => {

View File

@@ -1,11 +1,12 @@
import {
describe, beforeEach, afterEach, expect,
} from 'vitest';
import { ViteAppMetadata } from '@/infrastructure/Metadata/Vite/ViteAppMetadata';
import { VITE_ENVIRONMENT_KEYS } from '@/infrastructure/Metadata/Vite/ViteEnvironmentKeys';
import { VITE_ENVIRONMENT_KEYS } from '@/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentKeys';
import { PropertyKeys } from '@/TypeHelpers';
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
import { ViteEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/Vite/ViteEnvironmentVariables';
describe('ViteAppMetadata', () => {
describe('ViteEnvironmentVariables', () => {
describe('reads values from import.meta.env', () => {
let originalMetaEnv;
beforeEach(() => {
@@ -15,13 +16,15 @@ describe('ViteAppMetadata', () => {
Object.assign(import.meta.env, originalMetaEnv);
});
interface ITestCase {
readonly getActualValue: (sut: ViteAppMetadata) => string;
interface ITestCase<T> {
readonly getActualValue: (sut: IEnvironmentVariables) => T;
readonly environmentVariable: typeof VITE_ENVIRONMENT_KEYS[
keyof typeof VITE_ENVIRONMENT_KEYS];
readonly expected: string;
readonly expected: T;
}
const testCases: { readonly [K in PropertyKeys<ViteAppMetadata>]: ITestCase } = {
const testCases: {
readonly [K in PropertyKeys<IEnvironmentVariables>]: ITestCase<string | boolean>;
} = {
name: {
environmentVariable: VITE_ENVIRONMENT_KEYS.NAME,
expected: 'expected-name',
@@ -47,6 +50,11 @@ describe('ViteAppMetadata', () => {
expected: 'expected-homepageUrl',
getActualValue: (sut) => sut.homepageUrl,
},
isNonProduction: {
environmentVariable: VITE_ENVIRONMENT_KEYS.DEV,
expected: false,
getActualValue: (sut) => sut.isNonProduction,
},
};
Object.values(testCases).forEach(({ environmentVariable, expected, getActualValue }) => {
it(`should correctly get the value of ${environmentVariable}`, () => {
@@ -54,7 +62,7 @@ describe('ViteAppMetadata', () => {
import.meta.env[environmentVariable] = expected;
// act
const sut = new ViteAppMetadata();
const sut = new ViteEnvironmentVariables();
const actualValue = getActualValue(sut);
// assert

View File

@@ -0,0 +1,46 @@
import { describe, expect } from 'vitest';
import { StubWithObservableMethodCalls } from '@tests/unit/shared/Stubs/StubWithObservableMethodCalls';
import { ConsoleLogger } from '@/infrastructure/Log/ConsoleLogger';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachLoggingMethod } from './LoggerTestRunner';
describe('ConsoleLogger', () => {
describe('throws if console is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing console';
const console = absentValue;
// act
const act = () => new ConsoleLogger(console);
// assert
expect(act).to.throw(expectedError);
}, { excludeUndefined: true });
});
describe('methods log the provided params', () => {
itEachLoggingMethod((functionName, testParameters) => {
// arrange
const expectedParams = testParameters;
const consoleMock = new MockConsole();
const logger = new ConsoleLogger(consoleMock);
// act
logger[functionName](...expectedParams);
// assert
expect(consoleMock.callHistory).to.have.lengthOf(1);
expect(consoleMock.callHistory[0].methodName).to.equal(functionName);
expect(consoleMock.callHistory[0].args).to.deep.equal(expectedParams);
});
});
});
class MockConsole
extends StubWithObservableMethodCalls<Partial<Console>>
implements Partial<Console> {
public info(...args: unknown[]) {
this.registerMethodCall({
methodName: 'info',
args,
});
}
}

View File

@@ -0,0 +1,45 @@
import { describe, expect } from 'vitest';
import { ElectronLog } from 'electron-log';
import { StubWithObservableMethodCalls } from '@tests/unit/shared/Stubs/StubWithObservableMethodCalls';
import { createElectronLogger } from '@/infrastructure/Log/ElectronLogger';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachLoggingMethod } from './LoggerTestRunner';
describe('ElectronLogger', () => {
describe('throws if logger is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing logger';
const electronLog = absentValue;
// act
const act = () => createElectronLogger(electronLog);
// assert
expect(act).to.throw(expectedError);
}, { excludeUndefined: true });
});
itEachLoggingMethod((functionName, testParameters) => {
// arrange
const expectedParams = testParameters;
const electronLogMock = new MockElectronLog();
const logger = createElectronLogger(electronLogMock);
// act
logger[functionName](...expectedParams);
// assert
expect(electronLogMock.callHistory).to.have.lengthOf(1);
expect(electronLogMock.callHistory[0].methodName).to.equal(functionName);
expect(electronLogMock.callHistory[0].args).to.deep.equal(expectedParams);
});
});
class MockElectronLog
extends StubWithObservableMethodCalls<Partial<ElectronLog>>
implements Partial<ElectronLog> {
public info(...args: unknown[]) {
this.registerMethodCall({
methodName: 'info',
args,
});
}
}

View File

@@ -0,0 +1,21 @@
import { it } from 'vitest';
import { FunctionKeys } from '@/TypeHelpers';
import { ILogger } from '@/infrastructure/Log/ILogger';
export function itEachLoggingMethod(
handler: (
functionName: keyof ILogger,
testParameters?: unknown[]
) => void,
) {
const testParameters = ['test', 123, { some: 'object' }];
const loggerMethods: Array<FunctionKeys<ILogger>> = [
'info',
];
loggerMethods
.forEach((functionKey) => {
it(functionKey, () => {
handler(functionKey, testParameters);
});
});
}

View File

@@ -0,0 +1,20 @@
import { describe, expect } from 'vitest';
import { NoopLogger } from '@/infrastructure/Log/NoopLogger';
import { ILogger } from '@/infrastructure/Log/ILogger';
import { itEachLoggingMethod } from './LoggerTestRunner';
describe('NoopLogger', () => {
describe('methods do not throw', () => {
itEachLoggingMethod((functionName, testParameters) => {
// arrange
const randomParams = testParameters;
const logger: ILogger = new NoopLogger();
// act
const act = () => logger[functionName](...randomParams);
// assert
expect(act).to.not.throw();
});
});
});

View File

@@ -0,0 +1,50 @@
import { describe } from 'vitest';
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
import { WindowVariablesStub } from '@tests/unit/shared/Stubs/WindowVariablesStub';
import { WindowInjectedLogger } from '@/infrastructure/Log/WindowInjectedLogger';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachLoggingMethod } from './LoggerTestRunner';
describe('WindowInjectedLogger', () => {
describe('throws if log is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing log';
const windowVariables = new WindowVariablesStub()
.withLog(absentValue);
// act
const act = () => new WindowInjectedLogger(windowVariables);
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws if window is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing window';
const windowVariables = absentValue;
// act
const act = () => new WindowInjectedLogger(windowVariables);
// assert
expect(act).to.throw(expectedError);
}, { excludeUndefined: true });
});
describe('methods log the provided params', () => {
itEachLoggingMethod((functionName) => {
// arrange
const expectedParams = ['test', 123, { some: 'object' }];
const loggerMock = new LoggerStub();
const windowVariables = new WindowVariablesStub()
.withLog(loggerMock);
const logger = new WindowInjectedLogger(windowVariables);
// act
logger[functionName](...expectedParams);
// assert
expect(loggerMock.callHistory).to.have.lengthOf(1);
expect(loggerMock.callHistory[0].methodName).to.equal(functionName);
expect(loggerMock.callHistory[0].args).to.deep.equal(expectedParams);
});
});
});

View File

@@ -1,53 +0,0 @@
import {
describe,
} from 'vitest';
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
import { AppMetadataFactory, MetadataValidator } from '@/infrastructure/Metadata/AppMetadataFactory';
import { ViteAppMetadata } from '@/infrastructure/Metadata/Vite/ViteAppMetadata';
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
class TestableAppMetadataFactory extends AppMetadataFactory {
public constructor(validator: MetadataValidator = () => { /* NO OP */ }) {
super(validator);
}
}
describe('AppMetadataFactory', () => {
describe('instance', () => {
itIsSingleton({
getter: () => AppMetadataFactory.Current.instance,
expectedType: ViteAppMetadata,
});
});
it('creates the correct type of metadata', () => {
// arrange
const sut = new TestableAppMetadataFactory();
// act
const metadata = sut.instance;
// assert
expect(metadata).to.be.instanceOf(ViteAppMetadata);
});
it('validates its instance', () => {
// arrange
let validatedMetadata: IAppMetadata;
const validatorMock = (metadata: IAppMetadata) => {
validatedMetadata = metadata;
};
// act
const sut = new TestableAppMetadataFactory(validatorMock);
const actualInstance = sut.instance;
// assert
expect(actualInstance).to.equal(validatedMetadata);
});
it('throws error if validator fails', () => {
// arrange
const expectedError = 'validator failed';
const failingValidator = () => {
throw new Error(expectedError);
};
// act
const act = () => new TestableAppMetadataFactory(failingValidator);
// assert
expect(act).to.throw(expectedError);
});
});

View File

@@ -1,74 +0,0 @@
import { describe, it, expect } from 'vitest';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { AppMetadataStub } from '@tests/unit/shared/Stubs/AppMetadataStub';
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
import { validateMetadata } from '@/infrastructure/Metadata/MetadataValidator';
describe('MetadataValidator', () => {
it('does not throw if all metadata keys have values', () => {
// arrange
const metadata = new AppMetadataStub();
// act
const act = () => validateMetadata(metadata);
// assert
expect(act).to.not.throw();
});
describe('throws as expected', () => {
describe('"missing metadata" if metadata is not provided', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing metadata';
const metadata = absentValue;
// act
const act = () => validateMetadata(metadata);
// assert
expect(act).to.throw(expectedError);
});
});
it('"missing keys" if metadata has properties with missing values', () => {
// arrange
const expectedError = 'Metadata keys missing: name, homepageUrl';
const missingData: Partial<IAppMetadata> = {
name: undefined,
homepageUrl: undefined,
};
const metadata: IAppMetadata = {
...new AppMetadataStub(),
...missingData,
};
// act
const act = () => validateMetadata(metadata);
// assert
expect(act).to.throw(expectedError);
});
it('"missing keys" if metadata has getters with missing values', () => {
// arrange
const expectedError = 'Metadata keys missing: name, homepageUrl';
const stubWithGetters: Partial<IAppMetadata> = {
get name() {
return undefined;
},
get homepageUrl() {
return undefined;
},
};
const metadata: IAppMetadata = {
...new AppMetadataStub(),
...stubWithGetters,
};
// act
const act = () => validateMetadata(metadata);
// assert
expect(act).to.throw(expectedError);
});
it('"unable to capture metadata" if metadata has no getters or properties', () => {
// arrange
const expectedError = 'Unable to capture metadata key/value pairs';
const metadata = {} as IAppMetadata;
// act
const act = () => validateMetadata(metadata);
// assert
expect(act).to.throw(expectedError);
});
});
});

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { BrowserOsDetector } from '@/infrastructure/Environment/BrowserOs/BrowserOsDetector';
import { BrowserOsDetector } from '@/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsDetector';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { BrowserOsTestCases } from './BrowserOsTestCases';

View File

@@ -1,13 +1,13 @@
import { describe, it, expect } from 'vitest';
import { IBrowserOsDetector } from '@/infrastructure/Environment/BrowserOs/IBrowserOsDetector';
import { IBrowserOsDetector } from '@/infrastructure/RuntimeEnvironment/BrowserOs/IBrowserOsDetector';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { Environment, WindowValidator } from '@/infrastructure/Environment/Environment';
import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub';
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { WindowVariables } from '@/infrastructure/Environment/WindowVariables';
import { BrowserOsDetectorStub } from '@tests/unit/shared/Stubs/BrowserOsDetectorStub';
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
import { EnvironmentVariablesStub } from '@tests/unit/shared/Stubs/EnvironmentVariablesStub';
describe('Environment', () => {
describe('RuntimeEnvironment', () => {
describe('ctor', () => {
describe('throws if window is absent', () => {
itEachAbsentObjectValue((absentValue) => {
@@ -135,63 +135,21 @@ describe('Environment', () => {
});
});
});
describe('system', () => {
it('fetches system operations from window', () => {
// arrange
const expectedSystem = new SystemOperationsStub();
const windowWithSystem = {
system: expectedSystem,
};
// act
const sut = createEnvironment({
window: windowWithSystem,
describe('isNonProduction', () => {
[true, false].forEach((value) => {
it(`sets ${value} from environment variables`, () => {
// arrange
const expectedValue = value;
const environment = new EnvironmentVariablesStub()
.withIsNonProduction(expectedValue);
// act
const sut = createEnvironment({
environmentVariables: environment,
});
// assert
const actualValue = sut.isNonProduction;
expect(actualValue).to.equal(expectedValue);
});
// assert
const actualSystem = sut.system;
expect(actualSystem).to.equal(expectedSystem);
});
});
describe('validateWindow', () => {
it('throws when validator throws', () => {
// arrange
const expectedErrorMessage = 'expected error thrown from window validator';
const mockValidator: WindowValidator = () => {
throw new Error(expectedErrorMessage);
};
// act
const act = () => createEnvironment({
windowValidator: mockValidator,
});
// assert
expect(act).to.throw(expectedErrorMessage);
});
it('does not throw when validator does not throw', () => {
// arrange
const expectedErrorMessage = 'expected error thrown from window validator';
const mockValidator: WindowValidator = () => {
// do not throw
};
// act
const act = () => createEnvironment({
windowValidator: mockValidator,
});
// assert
expect(act).to.not.throw(expectedErrorMessage);
});
it('sends expected window to validator', () => {
// arrange
const expectedVariables: Partial<WindowVariables> = {};
let actualVariables: Partial<WindowVariables>;
const mockValidator: WindowValidator = (variables) => {
actualVariables = variables;
};
// act
createEnvironment({
window: expectedVariables,
windowValidator: mockValidator,
});
// assert
expect(actualVariables).to.equal(expectedVariables);
});
});
});
@@ -199,21 +157,21 @@ describe('Environment', () => {
interface EnvironmentOptions {
window: Partial<Window>;
browserOsDetector?: IBrowserOsDetector;
windowValidator?: WindowValidator;
environmentVariables?: IEnvironmentVariables;
}
function createEnvironment(options: Partial<EnvironmentOptions> = {}): TestableEnvironment {
function createEnvironment(options: Partial<EnvironmentOptions> = {}): TestableRuntimeEnvironment {
const defaultOptions: EnvironmentOptions = {
window: {},
browserOsDetector: new BrowserOsDetectorStub(),
windowValidator: () => { /* NO OP */ },
environmentVariables: new EnvironmentVariablesStub(),
};
return new TestableEnvironment({ ...defaultOptions, ...options });
return new TestableRuntimeEnvironment({ ...defaultOptions, ...options });
}
class TestableEnvironment extends Environment {
class TestableRuntimeEnvironment extends RuntimeEnvironment {
public constructor(options: EnvironmentOptions) {
super(options.window, options.browserOsDetector, options.windowValidator);
super(options.window, options.environmentVariables, options.browserOsDetector);
}
}

View File

@@ -0,0 +1,226 @@
import { describe, it, expect } from 'vitest';
import { validateWindowVariables } from '@/infrastructure/WindowVariables/WindowVariablesValidator';
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub';
import { getAbsentObjectTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
import { WindowVariablesStub } from '@tests/unit/shared/Stubs/WindowVariablesStub';
describe('WindowVariablesValidator', () => {
describe('validateWindowVariables', () => {
describe('validates window type', () => {
itEachInvalidObjectValue((invalidObjectValue) => {
// arrange
const expectedError = 'window is not an object';
const window: Partial<WindowVariables> = invalidObjectValue;
// act
const act = () => validateWindowVariables(window);
// assert
expect(act).to.throw(expectedError);
});
});
describe('property validations', () => {
it('throws an error with a description of all invalid properties', () => {
// arrange
const invalidOs = 'invalid' as unknown as OperatingSystem;
const invalidIsDesktop = 'not a boolean' as unknown as boolean;
const expectedError = getExpectedError(
{
name: 'os',
object: invalidOs,
},
{
name: 'isDesktop',
object: invalidIsDesktop,
},
);
const input = new WindowVariablesStub()
.withOs(invalidOs)
.withIsDesktop(invalidIsDesktop);
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.throw(expectedError);
});
describe('`os` property', () => {
it('throws an error when os is not a number', () => {
// arrange
const invalidOs = 'Linux' as unknown as OperatingSystem;
const expectedError = getExpectedError(
{
name: 'os',
object: invalidOs,
},
);
const input = new WindowVariablesStub()
.withOs(invalidOs);
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.throw(expectedError);
});
it('throws an error for an invalid numeric os value', () => {
// arrange
const invalidOs = Number.MAX_SAFE_INTEGER;
const expectedError = getExpectedError(
{
name: 'os',
object: invalidOs,
},
);
const input = new WindowVariablesStub()
.withOs(invalidOs);
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.throw(expectedError);
});
it('does not throw for a missing os value', () => {
// arrange
const input = new WindowVariablesStub()
.withIsDesktop(true)
.withOs(undefined);
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.not.throw();
});
});
describe('`isDesktop` property', () => {
it('throws an error when only isDesktop is provided and it is true without a system object', () => {
// arrange
const systemObject = undefined;
const expectedError = getExpectedError(
{
name: 'system',
object: systemObject,
},
);
const input = new WindowVariablesStub()
.withIsDesktop(true)
.withSystem(systemObject);
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.throw(expectedError);
});
it('does not throw when isDesktop is true with a valid system object', () => {
// arrange
const validSystem = new SystemOperationsStub();
const input = new WindowVariablesStub()
.withIsDesktop(true)
.withSystem(validSystem);
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.not.throw();
});
it('does not throw when isDesktop is false without a system object', () => {
// arrange
const absentSystem = undefined;
const input = new WindowVariablesStub()
.withIsDesktop(false)
.withSystem(absentSystem);
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.not.throw();
});
});
describe('`system` property', () => {
expectObjectOnDesktop('system');
});
describe('`log` property', () => {
expectObjectOnDesktop('log');
});
});
it('does not throw for a valid object', () => {
const input = new WindowVariablesStub();
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.not.throw();
});
});
});
function expectObjectOnDesktop<T>(key: keyof WindowVariables) {
describe('validates object type on desktop', () => {
itEachInvalidObjectValue((invalidObjectValue) => {
// arrange
const invalidObject = invalidObjectValue as T;
const expectedError = getExpectedError({
name: key,
object: invalidObject,
});
const input: WindowVariables = {
...new WindowVariablesStub(),
isDesktop: true,
[key]: invalidObject,
};
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.throw(expectedError);
});
});
describe('does not object type when not on desktop', () => {
itEachInvalidObjectValue((invalidObjectValue) => {
// arrange
const invalidObject = invalidObjectValue as T;
const input: WindowVariables = {
...new WindowVariablesStub(),
isDesktop: undefined,
[key]: invalidObject,
};
// act
const act = () => validateWindowVariables(input);
// assert
expect(act).to.not.throw();
});
});
}
function itEachInvalidObjectValue<T>(runner: (invalidObjectValue: T) => void) {
const testCases: Array<{
readonly name: string;
readonly value: T;
}> = [
{
name: 'given string',
value: 'invalid object' as unknown as T,
},
{
name: 'given array of objects',
value: [{}, {}] as unknown as T,
},
...getAbsentObjectTestCases().map((testCase) => ({
name: `given absent: ${testCase.valueName}`,
value: testCase.absentValue as unknown as T,
})),
];
testCases.forEach((testCase) => {
it(testCase.name, () => {
runner(testCase.value);
});
});
}
function getExpectedError(...unexpectedObjects: Array<{
readonly name: keyof WindowVariables;
readonly object: unknown;
}>) {
const errors = unexpectedObjects
.map(({ name, object }) => `Unexpected ${name} (${typeof object})`);
return errors.join('\n');
}

View File

@@ -1,7 +0,0 @@
import { describe } from 'vitest';
import { EnvironmentValidator } from '@/infrastructure/RuntimeSanity/Validators/EnvironmentValidator';
import { itNoErrorsOnCurrentEnvironment } from './ValidatorTestRunner';
describe('EnvironmentValidator', () => {
itNoErrorsOnCurrentEnvironment(() => new EnvironmentValidator());
});

View File

@@ -0,0 +1,17 @@
import { describe } from 'vitest';
import { EnvironmentVariablesValidator } from '@/infrastructure/RuntimeSanity/Validators/EnvironmentVariablesValidator';
import { FactoryFunction } from '@/infrastructure/RuntimeSanity/Common/FactoryValidator';
import { EnvironmentVariablesStub } from '@tests/unit/shared/Stubs/EnvironmentVariablesStub';
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
import { runFactoryValidatorTests } from './FactoryValidatorConcreteTestRunner';
describe('EnvironmentVariablesValidator', () => {
runFactoryValidatorTests({
createValidator: (
factory?: FactoryFunction<IEnvironmentVariables>,
) => new EnvironmentVariablesValidator(factory),
enablingOptionProperty: 'validateEnvironmentVariables',
factoryFunctionStub: () => new EnvironmentVariablesStub(),
expectedValidatorName: 'environment variables',
});
});

View File

@@ -1,7 +0,0 @@
import { describe } from 'vitest';
import { MetadataValidator } from '@/infrastructure/RuntimeSanity/Validators/MetadataValidator';
import { itNoErrorsOnCurrentEnvironment } from './ValidatorTestRunner';
describe('MetadataValidator', () => {
itNoErrorsOnCurrentEnvironment(() => new MetadataValidator());
});

View File

@@ -0,0 +1,17 @@
import { describe } from 'vitest';
import { WindowVariablesValidator } from '@/infrastructure/RuntimeSanity/Validators/WindowVariablesValidator';
import { FactoryFunction } from '@/infrastructure/RuntimeSanity/Common/FactoryValidator';
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
import { WindowVariablesStub } from '@tests/unit/shared/Stubs/WindowVariablesStub';
import { runFactoryValidatorTests } from './FactoryValidatorConcreteTestRunner';
describe('WindowVariablesValidator', () => {
runFactoryValidatorTests({
createValidator: (
factory?: FactoryFunction<WindowVariables>,
) => new WindowVariablesValidator(factory),
enablingOptionProperty: 'validateWindowVariables',
factoryFunctionStub: () => new WindowVariablesStub(),
expectedValidatorName: 'window variables',
});
});

View File

@@ -0,0 +1,46 @@
import { describe, it, expect } from 'vitest';
import { getWindowInjectedSystemOperations } from '@/infrastructure/SystemOperations/WindowInjectedSystemOperations';
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub';
describe('WindowInjectedSystemOperations', () => {
describe('getWindowInjectedSystemOperations', () => {
describe('throws if window is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing window';
const window: WindowVariables = absentValue;
// act
const act = () => getWindowInjectedSystemOperations(window);
// assert
expect(act).to.throw(expectedError);
}, { excludeUndefined: true });
});
describe('throw if system is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing system';
const absentSystem = absentValue;
const window: Partial<WindowVariables> = {
system: absentSystem,
};
// act
const act = () => getWindowInjectedSystemOperations(window);
// assert
expect(act).to.throw(expectedError);
});
});
it('returns from window', () => {
// arrange
const expectedValue = new SystemOperationsStub();
const window: Partial<WindowVariables> = {
system: expectedValue,
};
// act
const actualValue = getWindowInjectedSystemOperations(window);
// assert
expect(actualValue).to.equal(expectedValue);
});
});
});

View File

@@ -0,0 +1,80 @@
import {
describe, it, beforeEach, afterEach,
} from 'vitest';
import { IRuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/IRuntimeEnvironment';
import { ClientLoggerFactory } from '@/presentation/bootstrapping/ClientLoggerFactory';
import { ILogger } from '@/infrastructure/Log/ILogger';
import { WindowInjectedLogger } from '@/infrastructure/Log/WindowInjectedLogger';
import { ConsoleLogger } from '@/infrastructure/Log/ConsoleLogger';
import { NoopLogger } from '@/infrastructure/Log/NoopLogger';
import { RuntimeEnvironmentStub } from '@tests/unit/shared/Stubs/RuntimeEnvironmentStub';
import { Constructible } from '@/TypeHelpers';
import { itIsSingleton } from '@tests/unit/shared/TestCases/SingletonTests';
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
describe('ClientLoggerFactory', () => {
describe('Current', () => {
itIsSingleton({
getter: () => ClientLoggerFactory.Current,
expectedType: ClientLoggerFactory,
});
});
describe('logger instantiation based on environment', () => {
const originalWindow = { ...window };
beforeEach(() => {
Object.assign(window, { log: new LoggerStub() });
});
afterEach(() => {
Object.assign(window, originalWindow);
});
const testCases: Array<{
readonly description: string,
readonly expectedType: Constructible<ILogger>,
readonly environment: IRuntimeEnvironment,
}> = [
{
description: 'desktop environment',
expectedType: WindowInjectedLogger,
environment: new RuntimeEnvironmentStub()
.withIsDesktop(true),
},
{
description: 'non-production and desktop environment',
expectedType: WindowInjectedLogger,
environment: new RuntimeEnvironmentStub()
.withIsDesktop(true)
.withIsNonProduction(true),
},
{
description: 'non-production without desktop',
expectedType: ConsoleLogger,
environment: new RuntimeEnvironmentStub()
.withIsDesktop(false)
.withIsNonProduction(true),
},
{
description: 'production without desktop',
expectedType: NoopLogger,
environment: new RuntimeEnvironmentStub()
.withIsDesktop(false)
.withIsNonProduction(false),
},
];
testCases.forEach(({ description, expectedType, environment }) => {
it(`instantiates ${expectedType.name} for ${description}`, () => {
// arrange
const factory = new TestableClientLoggerFactory(environment);
// act
const { logger } = factory;
// assert
expect(logger).to.be.instanceOf(expectedType);
});
});
});
});
class TestableClientLoggerFactory extends ClientLoggerFactory {
public constructor(environment: IRuntimeEnvironment) {
super(environment);
}
}

View File

@@ -0,0 +1,18 @@
import { describe, it, expect } from 'vitest';
import { AppInitializationLogger } from '@/presentation/bootstrapping/Modules/AppInitializationLogger';
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
describe('AppInitializationLogger', () => {
it('logs the app initialization marker upon bootstrap', () => {
// arrange
const marker = '[APP_INIT]';
const loggerStub = new LoggerStub();
const sut = new AppInitializationLogger(loggerStub);
// act
sut.bootstrap();
// assert
expect(loggerStub.callHistory).to.have.lengthOf(1);
expect(loggerStub.callHistory[0].args).to.have.lengthOf(1);
expect(loggerStub.callHistory[0].args[0]).to.include(marker);
});
});

View File

@@ -0,0 +1,43 @@
import { describe, it, expect } from 'vitest';
import { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions';
import { RuntimeSanityValidator } from '@/presentation/bootstrapping/Modules/RuntimeSanityValidator';
describe('RuntimeSanityValidator', () => {
it('calls validator with correct options upon bootstrap', () => {
// arrange
const expectedOptions: ISanityCheckOptions = {
validateEnvironmentVariables: true,
validateWindowVariables: true,
};
let actualOptions: ISanityCheckOptions;
const validatorMock = (options) => {
actualOptions = options;
};
const sut = new RuntimeSanityValidator(validatorMock);
// act
sut.bootstrap();
// assert
expect(actualOptions).to.deep.equal(expectedOptions);
});
it('propagates the error if validator fails', () => {
// arrange
const expectedMessage = 'message thrown from validator';
const validatorMock = () => {
throw new Error(expectedMessage);
};
const sut = new RuntimeSanityValidator(validatorMock);
// act
const act = () => sut.bootstrap();
// assert
expect(act).to.throw(expectedMessage);
});
it('runs successfully if validator passes', () => {
// arrange
const validatorMock = () => { /* NOOP */ };
const sut = new RuntimeSanityValidator(validatorMock);
// act
const act = () => sut.bootstrap();
// assert
expect(act).to.not.throw();
});
});

View File

@@ -1,16 +1,16 @@
import { describe, it, expect } from 'vitest';
import { useEnvironment } from '@/presentation/components/Shared/Hooks/UseEnvironment';
import { useRuntimeEnvironment } from '@/presentation/components/Shared/Hooks/UseRuntimeEnvironment';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { EnvironmentStub } from '@tests/unit/shared/Stubs/EnvironmentStub';
import { RuntimeEnvironmentStub } from '@tests/unit/shared/Stubs/RuntimeEnvironmentStub';
describe('UseEnvironment', () => {
describe('UseRuntimeEnvironment', () => {
describe('environment is absent', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing environment';
const environmentValue = absentValue;
// act
const act = () => useEnvironment(environmentValue);
const act = () => useRuntimeEnvironment(environmentValue);
// assert
expect(act).to.throw(expectedError);
});
@@ -18,9 +18,9 @@ describe('UseEnvironment', () => {
it('returns expected environment', () => {
// arrange
const expectedEnvironment = new EnvironmentStub();
const expectedEnvironment = new RuntimeEnvironmentStub();
// act
const actualEnvironment = useEnvironment(expectedEnvironment);
const actualEnvironment = useRuntimeEnvironment(expectedEnvironment);
// assert
expect(actualEnvironment).to.equal(expectedEnvironment);
});

View File

@@ -2,11 +2,13 @@ import { describe, it, expect } from 'vitest';
import { provideWindowVariables } from '@/presentation/electron/preload/WindowVariablesProvider';
import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { ISystemOperations } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations';
import { ILogger } from '@/infrastructure/Log/ILogger';
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
describe('WindowVariablesProvider', () => {
describe('provideWindowVariables', () => {
it('returns expected system', () => {
it('returns expected `system`', () => {
// arrange
const expectedValue = new SystemOperationsStub();
// act
@@ -16,7 +18,7 @@ describe('WindowVariablesProvider', () => {
// assert
expect(variables.system).to.equal(expectedValue);
});
it('returns expected os', () => {
it('returns expected `os`', () => {
// arrange
const expectedValue = OperatingSystem.WindowsPhone;
// act
@@ -26,6 +28,16 @@ describe('WindowVariablesProvider', () => {
// assert
expect(variables.os).to.equal(expectedValue);
});
it('returns expected `log`', () => {
// arrange
const expectedValue = new LoggerStub();
// act
const variables = new TestContext()
.withLogger(expectedValue)
.provideWindowVariables();
// assert
expect(variables.log).to.equal(expectedValue);
});
it('`isDesktop` is true', () => {
// arrange
const expectedValue = true;
@@ -43,6 +55,8 @@ class TestContext {
private os: OperatingSystem = OperatingSystem.Android;
private log: ILogger = new LoggerStub();
public withSystem(system: ISystemOperations): this {
this.system = system;
return this;
@@ -53,9 +67,15 @@ class TestContext {
return this;
}
public withLogger(log: ILogger): this {
this.log = log;
return this;
}
public provideWindowVariables() {
return provideWindowVariables(
() => this.system,
() => this.log,
() => this.os,
);
}

View File

@@ -1,4 +1,4 @@
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
export class AppMetadataStub implements IAppMetadata {
public version = '0.12.2';

View File

@@ -1,5 +1,5 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IBrowserOsDetector } from '@/infrastructure/Environment/BrowserOs/IBrowserOsDetector';
import { IBrowserOsDetector } from '@/infrastructure/RuntimeEnvironment/BrowserOs/IBrowserOsDetector';
export class BrowserOsDetectorStub implements IBrowserOsDetector {
public detect(): OperatingSystem {

View File

@@ -1,4 +1,4 @@
import { ICommandOps } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
import { ICommandOps } from '@/infrastructure/SystemOperations/ISystemOperations';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
export class CommandOpsStub

View File

@@ -1,22 +0,0 @@
import { IEnvironment } from '@/infrastructure/Environment/IEnvironment';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { ISystemOperations } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
import { SystemOperationsStub } from './SystemOperationsStub';
export class EnvironmentStub implements IEnvironment {
public isDesktop = true;
public os = OperatingSystem.Windows;
public system: ISystemOperations = new SystemOperationsStub();
public withOs(os: OperatingSystem): this {
this.os = os;
return this;
}
public withSystemOperations(system: ISystemOperations): this {
this.system = system;
return this;
}
}

View File

@@ -0,0 +1,11 @@
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables';
import { AppMetadataStub } from './AppMetadataStub';
export class EnvironmentVariablesStub extends AppMetadataStub implements IEnvironmentVariables {
public isNonProduction = true;
public withIsNonProduction(isNonProduction: boolean): this {
this.isNonProduction = isNonProduction;
return this;
}
}

View File

@@ -1,4 +1,4 @@
import { IFileSystemOps } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
import { IFileSystemOps } from '@/infrastructure/SystemOperations/ISystemOperations';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
export class FileSystemOpsStub

View File

@@ -1,4 +1,4 @@
import { ILocationOps } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
import { ILocationOps } from '@/infrastructure/SystemOperations/ISystemOperations';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
export class LocationOpsStub

View File

@@ -0,0 +1,12 @@
import { ILogger } from '@/infrastructure/Log/ILogger';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
export class LoggerStub extends StubWithObservableMethodCalls<ILogger> implements ILogger {
public info(...params: unknown[]): void {
this.registerMethodCall({
methodName: 'info',
args: params,
});
console.log(...params);
}
}

View File

@@ -1,4 +1,4 @@
import { IOperatingSystemOps } from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
import { IOperatingSystemOps } from '@/infrastructure/SystemOperations/ISystemOperations';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
export class OperatingSystemOpsStub

View File

@@ -1,5 +1,5 @@
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
import { IAppMetadata } from '@/infrastructure/Metadata/IAppMetadata';
import { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
import { IProjectInformation } from '@/domain/IProjectInformation';
import { ProjectInformationStub } from './ProjectInformationStub';

View File

@@ -0,0 +1,25 @@
import { IRuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/IRuntimeEnvironment';
import { OperatingSystem } from '@/domain/OperatingSystem';
export class RuntimeEnvironmentStub implements IRuntimeEnvironment {
public isNonProduction = true;
public isDesktop = true;
public os = OperatingSystem.Windows;
public withOs(os: OperatingSystem): this {
this.os = os;
return this;
}
public withIsDesktop(isDesktop: boolean): this {
this.isDesktop = isDesktop;
return this;
}
public withIsNonProduction(isNonProduction: boolean): this {
this.isNonProduction = isNonProduction;
return this;
}
}

View File

@@ -1,17 +1,17 @@
import { ISanityCheckOptions } from '@/infrastructure/RuntimeSanity/Common/ISanityCheckOptions';
export class SanityCheckOptionsStub implements ISanityCheckOptions {
public validateEnvironment = false;
public validateWindowVariables = false;
public validateMetadata = false;
public validateEnvironmentVariables = false;
public withValidateMetadata(value: boolean): this {
this.validateMetadata = value;
public withvalidateEnvironmentVariables(value: boolean): this {
this.validateEnvironmentVariables = value;
return this;
}
public withValidateEnvironment(value: boolean): this {
this.validateEnvironment = value;
this.validateWindowVariables = value;
return this;
}
}

View File

@@ -4,7 +4,7 @@ import {
IOperatingSystemOps,
ILocationOps,
ISystemOperations,
} from '@/infrastructure/Environment/SystemOperations/ISystemOperations';
} from '@/infrastructure/SystemOperations/ISystemOperations';
import { CommandOpsStub } from './CommandOpsStub';
import { FileSystemOpsStub } from './FileSystemOpsStub';
import { LocationOpsStub } from './LocationOpsStub';

View File

@@ -0,0 +1,36 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { ILogger } from '@/infrastructure/Log/ILogger';
import { ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations';
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
import { SystemOperationsStub } from './SystemOperationsStub';
import { LoggerStub } from './LoggerStub';
export class WindowVariablesStub implements WindowVariables {
public system: ISystemOperations = new SystemOperationsStub();
public isDesktop = false;
public os: OperatingSystem = OperatingSystem.BlackBerryOS;
public log: ILogger = new LoggerStub();
public withLog(log: ILogger): this {
this.log = log;
return this;
}
public withIsDesktop(value: boolean): this {
this.isDesktop = value;
return this;
}
public withOs(value: OperatingSystem): this {
this.os = value;
return this;
}
public withSystem(value: ISystemOperations): this {
this.system = value;
return this;
}
}