Files
privacy.sexy/tests/unit/infrastructure/RuntimeEnvironment/BrowserOs/ConditionBasedOsDetector.spec.ts
undergroundwires a9851272ae Fix touch state not being activated in iOS Safari
This commit resolves the issue with the `:active` pseudo-class not
activating in mobile Safari on iOS devices. It introduces a workaround
specifically for mobile Safari on iOS/iPadOS to enable the `:active`
pseudo-class. This ensures a consistent and responsive user interface
in response to touch states on mobile Safari.

Other supporting changes:

- Introduce new test utility functions such as `createWindowEventSpies`
  and `formatAssertionMessage` to improve code reusability and
  maintainability.
- Improve browser detection:
  - Add detection for iPadOS and Windows 10 Mobile.
  - Add touch support detection to correctly determine iPadOS vs macOS.
  - Fix misidentification of some Windows 10 Mobile platforms as Windows
    Phone.
  - Improve test coverage and refactor tests.
2023-12-11 05:24:27 +01:00

261 lines
11 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { BrowserCondition, TouchSupportExpectation } from '@/infrastructure/RuntimeEnvironment/BrowserOs/BrowserCondition';
import { ConditionBasedOsDetector } from '@/infrastructure/RuntimeEnvironment/BrowserOs/ConditionBasedOsDetector';
import { getAbsentStringTestCases, itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { BrowserEnvironmentStub } from '@tests/unit/shared/Stubs/BrowserEnvironmentStub';
import { BrowserConditionStub } from '@tests/unit/shared/Stubs/BrowserConditionStub';
import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTestRunner';
describe('ConditionBasedOsDetector', () => {
describe('constructor', () => {
describe('throws when given no conditions', () => {
itEachAbsentCollectionValue<BrowserCondition>((absentCollection) => {
// arrange
const expectedError = 'empty conditions';
const conditions = absentCollection;
// act
const act = () => new ConditionBasedOsDetectorBuilder()
.withConditions(conditions)
.build();
// assert
expect(act).to.throw(expectedError);
}, { excludeUndefined: true, excludeNull: true });
});
it('throws if user agent part is missing', () => {
// arrange
const expectedError = 'Each condition must include at least one identifiable part of the user agent string.';
const invalidCondition = new BrowserConditionStub().withExistingPartsInSameUserAgent([]);
// act
const act = () => new ConditionBasedOsDetectorBuilder()
.withConditions([invalidCondition])
.build();
// assert
expect(act).to.throw(expectedError);
});
describe('validates touch support expectation range', () => {
// arrange
const validValue = TouchSupportExpectation.MustExist;
// act
const act = (touchSupport: TouchSupportExpectation) => new ConditionBasedOsDetectorBuilder()
.withConditions([new BrowserConditionStub().withTouchSupport(touchSupport)])
.build();
// assert
new EnumRangeTestRunner(act)
.testOutOfRangeThrows()
.testValidValueDoesNotThrow(validValue);
});
it('throws if duplicate parts exist in user agent', () => {
// arrange
const expectedError = 'Found duplicate entries in user agent parts: Windows. Each part should be unique.';
const invalidCondition = {
operatingSystem: OperatingSystem.Windows,
existingPartsInSameUserAgent: ['Windows', 'Windows'],
};
// act
const act = () => new ConditionBasedOsDetectorBuilder()
.withConditions([invalidCondition])
.build();
// assert
expect(act).toThrowError(expectedError);
});
it('throws if duplicate non-existing parts exist in user agent', () => {
// arrange
const expectedError = 'Found duplicate entries in user agent parts: Linux. Each part should be unique.';
const invalidCondition = {
operatingSystem: OperatingSystem.Linux,
existingPartsInSameUserAgent: ['Linux'],
notExistingPartsInUserAgent: ['Linux'],
};
// act
const act = () => new ConditionBasedOsDetectorBuilder()
.withConditions([invalidCondition])
.build();
// assert
expect(act).toThrowError(expectedError);
});
it('throws if duplicates found in any user agent parts', () => {
// arrange
const expectedError = 'Found duplicate entries in user agent parts: Android. Each part should be unique.';
const invalidCondition = {
operatingSystem: OperatingSystem.Android,
existingPartsInSameUserAgent: ['Android'],
notExistingPartsInUserAgent: ['iOS', 'Android'],
};
// act
const act = () => new ConditionBasedOsDetectorBuilder()
.withConditions([invalidCondition])
.build();
// assert
expect(act).toThrowError(expectedError);
});
});
describe('detect', () => {
it('detects the correct OS when multiple conditions match', () => {
// arrange
const expectedOperatingSystem = OperatingSystem.Linux;
const testUserAgent = 'test-user-agent';
const expectedCondition = new BrowserConditionStub()
.withOperatingSystem(expectedOperatingSystem)
.withExistingPartsInSameUserAgent([testUserAgent]);
const conditions = [
expectedCondition,
new BrowserConditionStub()
.withExistingPartsInSameUserAgent(['unrelated user agent'])
.withOperatingSystem(OperatingSystem.Android),
new BrowserConditionStub()
.withNotExistingPartsInUserAgent([testUserAgent])
.withOperatingSystem(OperatingSystem.macOS),
];
const environment = new BrowserEnvironmentStub()
.withUserAgent(testUserAgent);
const detector = new ConditionBasedOsDetectorBuilder()
.withConditions(conditions)
.build();
// act
const actualOperatingSystem = detector.detect(environment);
// assert
expect(actualOperatingSystem).to.equal(expectedOperatingSystem);
});
describe('user agent checks', () => {
const testScenarios: ReadonlyArray<{
readonly description: string;
readonly buildEnvironment: (environment: BrowserEnvironmentStub) => BrowserEnvironmentStub;
readonly buildCondition: (condition: BrowserConditionStub) => BrowserConditionStub;
readonly detects: boolean;
}> = [
...getAbsentStringTestCases({ excludeUndefined: true, excludeNull: true })
.map((testCase) => ({
description: `does not detect when user agent is empty (${testCase.valueName})`,
buildEnvironment: (environment) => environment.withUserAgent(testCase.absentValue),
buildCondition: (condition) => condition,
detects: false,
})),
{
description: 'detects when user agent matches completely',
buildEnvironment: (environment) => environment.withUserAgent('test-user-agent'),
buildCondition: (condition) => condition.withExistingPartsInSameUserAgent(['test-user-agent']),
detects: true,
},
{
description: 'detects when substring of user agent exists',
buildEnvironment: (environment) => environment.withUserAgent('test-user-agent'),
buildCondition: (condition) => condition.withExistingPartsInSameUserAgent(['test']),
detects: true,
},
{
description: 'does not detect when no part of user agent exists',
buildEnvironment: (environment) => environment.withUserAgent('unrelated-user-agent'),
buildCondition: (condition) => condition.withExistingPartsInSameUserAgent(['lorem-ipsum']),
detects: false,
},
{
description: 'detects when non-existing parts do not match',
buildEnvironment: (environment) => environment.withUserAgent('1-3'),
buildCondition: (condition) => condition.withExistingPartsInSameUserAgent(['1']).withNotExistingPartsInUserAgent(['2']),
detects: true,
},
{
description: 'does not detect when non-existing and existing parts match',
buildEnvironment: (environment) => environment.withUserAgent('1-2'),
buildCondition: (condition) => condition.withExistingPartsInSameUserAgent(['1']).withNotExistingPartsInUserAgent(['2']),
detects: false,
},
];
testScenarios.forEach(({
description, buildEnvironment, buildCondition, detects,
}) => {
it(description, () => {
// arrange
const environment = buildEnvironment(new BrowserEnvironmentStub());
const condition = buildCondition(
new BrowserConditionStub().withOperatingSystem(OperatingSystem.Linux),
);
const detector = new ConditionBasedOsDetectorBuilder()
.withConditions([condition])
.build();
// act
const actualOperatingSystem = detector.detect(environment);
// assert
expect(actualOperatingSystem !== undefined).to.equal(detects);
});
});
});
describe('touch support checks', () => {
const testScenarios: ReadonlyArray<{
readonly description: string;
readonly expectation: TouchSupportExpectation;
readonly isTouchSupportInEnvironment: boolean;
readonly detects: boolean;
}> = [
{
description: 'detects when touch support exists and is expected',
expectation: TouchSupportExpectation.MustExist,
isTouchSupportInEnvironment: true,
detects: true,
},
{
description: 'does not detect when touch support does not exists but is expected',
expectation: TouchSupportExpectation.MustExist,
isTouchSupportInEnvironment: false,
detects: false,
},
{
description: 'detects when touch support does not exist and is not expected',
expectation: TouchSupportExpectation.MustNotExist,
isTouchSupportInEnvironment: false,
detects: true,
},
{
description: 'does not detect when touch support exists but is not expected',
expectation: TouchSupportExpectation.MustNotExist,
isTouchSupportInEnvironment: true,
detects: false,
},
];
testScenarios.forEach(({
description, expectation, isTouchSupportInEnvironment, detects,
}) => {
it(description, () => {
// arrange
const userAgent = 'iPhone';
const environment = new BrowserEnvironmentStub()
.withUserAgent(userAgent)
.withIsTouchSupported(isTouchSupportInEnvironment);
const conditionWithTouchSupport = new BrowserConditionStub()
.withExistingPartsInSameUserAgent([userAgent])
.withTouchSupport(expectation);
const detector = new ConditionBasedOsDetectorBuilder()
.withConditions([conditionWithTouchSupport])
.build();
// act
const actualOperatingSystem = detector.detect(environment);
// assert
expect(actualOperatingSystem !== undefined)
.to.equal(detects);
});
});
});
});
});
class ConditionBasedOsDetectorBuilder {
private conditions: readonly BrowserCondition[] = [{
operatingSystem: OperatingSystem.iOS,
existingPartsInSameUserAgent: ['iPhone'],
}];
public withConditions(conditions: readonly BrowserCondition[]): this {
this.conditions = conditions;
return this;
}
public build(): ConditionBasedOsDetector {
return new ConditionBasedOsDetector(
this.conditions,
);
}
}