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.
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user