Skip to content

Commit

Permalink
Merge pull request #13 from Miki-Session/consistent-account-colors
Browse files Browse the repository at this point in the history
Consistent avatar colors
  • Loading branch information
burtonemily authored Jan 17, 2025
2 parents b9e833c + 312cc67 commit f6b263f
Show file tree
Hide file tree
Showing 10 changed files with 530 additions and 137 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@
"eslint": "^9.14.0",
"globals": "^15.12.0",
"lodash": "^4.17.21",
"looks-same": "^9.0.1",
"png-js": "^1.0.0",
"prettier": "^3.3.3",
"sinon": "^19.0.2",
"ts-node": "^10.9.1",
"typescript": "^5.6.3",
"typescript-eslint": "^8.14.0",
"typescript-eslint": "^8.15.0",
"wd": "^1.14.0",
"wdio-wait-for": "^2.2.6"
},
Expand Down
42 changes: 42 additions & 0 deletions run/test/specs/check_avatar_color.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { bothPlatformsIt } from '../../types/sessionIt';
import { USERNAME } from '../../types/testing';
import { newUser } from './utils/create_account';
import { newContact } from './utils/create_contact';
import { SupportedPlatformsType, closeApp, openAppTwoDevices } from './utils/open_app';
import { isSameColor } from './utils/check_colour';
import { UserSettings } from './locators/settings';
import { ConversationItem } from './locators/home';
import { ConversationAvatar, ConversationSettings } from './locators/conversation';

bothPlatformsIt('Avatar color', 'medium', avatarColor);

async function avatarColor(platform: SupportedPlatformsType) {
const { device1, device2 } = await openAppTwoDevices(platform);
const [userA, userB] = await Promise.all([
newUser(device1, USERNAME.ALICE),
newUser(device2, USERNAME.BOB),
]);
await newContact(platform, device1, userA, device2, userB);
await Promise.all([device1.navigateBack(), device2.navigateBack()]);
// Get Alice's avatar color on device 1 (Home Screen avatar) and turn it into a hex value
const device1PixelColor = await device1.getElementPixelColor(new UserSettings(device1));
// Get Alice's avatar color on device 2 and turn it into a hex value
await device2.clickOnElementAll(new ConversationItem(device2));
let device2PixelColor;
// The conversation screen looks slightly different per platform so we're grabbing the avatar from different locators
// On iOS the avatar doubles as the Conversation Settings button on the right
// On Android, the avatar is a separate, non-interactable element on the left (and the settings has the 3-dot icon)
if (platform === 'ios') {
device2PixelColor = await device2.getElementPixelColor(new ConversationSettings(device2));
} else {
device2PixelColor = await device2.getElementPixelColor(new ConversationAvatar(device2));
}
// Color matching devices 1 and 2
const colorMatch = isSameColor(device1PixelColor, device2PixelColor);
if (!colorMatch) {
throw new Error(
`The avatar color of ${userA.userName} does not match across devices. The colors are ${device1PixelColor} and ${device2PixelColor}`
);
}
await closeApp(device1, device2);
}
25 changes: 25 additions & 0 deletions run/test/specs/linked_device_avatar_color.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { bothPlatformsIt } from '../../types/sessionIt';
import { USERNAME } from '../../types/testing';
import { linkedDevice } from './utils/link_device';
import { SupportedPlatformsType, closeApp, openAppTwoDevices } from './utils/open_app';
import { isSameColor } from './utils/check_colour';
import { UserSettings } from './locators/settings';

bothPlatformsIt('Avatar color linked device', 'medium', avatarColorLinkedDevice);

async function avatarColorLinkedDevice(platform: SupportedPlatformsType) {
const { device1, device2 } = await openAppTwoDevices(platform);
const userA = await linkedDevice(device1, device2, USERNAME.ALICE);
// Get Alice's avatar color on device 1 (Home Screen avatar) and turn it into a hex value
const device1PixelColor = await device1.getElementPixelColor(new UserSettings(device1));
// Get Alice's avatar color on the linked device (Home Screen avatar) and turn it into a hex value
const device2PixelColor = await device2.getElementPixelColor(new UserSettings(device2));
// Color matching devices 1 and 2
const colorMatch = isSameColor(device1PixelColor, device2PixelColor);
if (!colorMatch) {
throw new Error(
`The avatar color of ${userA.userName} does not match across devices. The colors are ${device1PixelColor} and ${device2PixelColor}`
);
}
await closeApp(device1, device2);
}
24 changes: 24 additions & 0 deletions run/test/specs/locators/conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,27 @@ export class MessageInput extends LocatorsInterface {
} as const;
}
}

export class ConversationSettings extends LocatorsInterface {
public build() {
return {
strategy: 'accessibility id',
selector: 'More options',
} as const;
}
}

// android-only locator for the avatar
export class ConversationAvatar extends LocatorsInterface {
public build() {
switch (this.platform) {
case 'android':
return {
strategy: 'id',
selector: 'network.loki.messenger:id/singleModeImageView',
} as const;
case 'ios':
throw new Error('Unsupported platform');
}
}
}
10 changes: 10 additions & 0 deletions run/test/specs/locators/home.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { StrategyExtractionObj } from '../../../types/testing';
import { LocatorsInterface } from './index';

export class ConversationItem extends LocatorsInterface {
public build(text?: string) {
return {
strategy: 'accessibility id',
selector: 'Conversation list item',
text: text,
} as const;
}
}

export class PlusButton extends LocatorsInterface {
public build() {
return {
Expand Down
12 changes: 12 additions & 0 deletions run/test/specs/utils/check_colour.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import PNG from 'png-js';
import { colors } from 'looks-same';
import { hexToRgbObject } from './utilities';

export async function parseDataImage(base64: string) {
const buffer = Buffer.from(base64, 'base64');
Expand All @@ -23,3 +25,13 @@ export async function parseDataImage(base64: string) {
// console.info("Middle x:", middleX, "middleY:", middleY, "width:", width);
return pixelColor;
}

// Determines if two colors look "the same" for humans even if they are not an exact match
export function isSameColor(hex1: string, hex2: string) {
// Convert the hex strings to RGB objects
const rgb1 = hexToRgbObject(hex1);
const rgb2 = hexToRgbObject(hex2);
// Perform the color comparison using the looks-same library
const isSameColor = colors(rgb1, rgb2);
return isSameColor;
}
13 changes: 13 additions & 0 deletions run/test/specs/utils/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,16 @@ export const isDeviceAndroid = (device: unknown) => !isDeviceIOS(device);
export const isCI = () => {
return process.env.NODE_CONFIG_ENV === 'ci';
};

// Converts a hexadecimal color string to an RGB object
export function hexToRgbObject(hex: string): { R: number; G: number; B: number } {
// Parse the hexadecimal string into a decimal number
// Removes the # prefix if present and converts the remaining string to base-10
const decimalValue = parseInt(hex.replace('#', ''), 16);
// Extract the red, green, and blue components using bitwise operations
return {
R: (decimalValue >> 16) & 255,
G: (decimalValue >> 8) & 255,
B: decimalValue & 255,
};
}
15 changes: 15 additions & 0 deletions run/types/DeviceWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
User,
XPath,
} from './testing';
import { parseDataImage } from '../test/specs/utils/check_colour';

export type Coordinates = {
x: number;
Expand Down Expand Up @@ -1787,6 +1788,20 @@ export class DeviceWrapper {
}
}

public async getElementPixelColor(
args: {
text?: string;
maxWait?: number;
} & (StrategyExtractionObj | LocatorsInterface)
): Promise<string> {
// Wait for the element to be present
const element = await this.waitForTextElementToBePresent(args);
// Take a screenshot and return a hex color value
const base64image = await this.getElementScreenshot(element.ELEMENT);
const pixelColor = await parseDataImage(base64image);
return pixelColor;
}

/* === all the utilities function === */
public isIOS(): boolean {
return isDeviceIOS(this.device);
Expand Down
3 changes: 2 additions & 1 deletion run/types/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ export type Id =
| 'com.android.chrome:id/signin_fre_dismiss_button'
| 'com.android.chrome:id/negative_button'
| 'network.loki.messenger:id/back_button'
| 'Quit';
| 'Quit'
| 'network.loki.messenger:id/singleModeImageView';

export type TestRisk = 'high' | 'medium' | 'low' | undefined;
Loading

0 comments on commit f6b263f

Please sign in to comment.