Skip to content

feat: device-aware logging + screenshots on test failure #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

Miki-Session
Copy link
Collaborator

@Miki-Session Miki-Session commented Jun 28, 2025

  • Adds automatic screenshot capture when tests fail
    • On test failure, captures screenshots from all devices
    • Automatically composites them into a single image (side-by-side for 2 devices, grid for 3-4)
    • Labels each device's screenshot for easy identification (alice1, alice2, bob1, etc)
    • example screenshot
  • Each DeviceWrapper instance now prefixes its logs with a device identifier
    • Device identities are set automatically in state builders and utility functions
    • Logs show clear device names like [alice1], [bob1]
[alice1] Waiting for "accessibility id" and "Restore your session button" to be present
[alice2] Waiting for "accessibility id" and "Restore your session button" to be present
[alice2] 'Restore your session button' has been found
[alice1] 'Restore your session button' has been found
  • Makes testInfo: TestInfo a mandatory parameter for all tests

@Miki-Session Miki-Session changed the title feat: screenshots on failed test and enhanced logging feat: device-aware logging + screenshots on test failure Jun 28, 2025
@Miki-Session Miki-Session marked this pull request as ready for review June 30, 2025 03:47
@Miki-Session Miki-Session requested a review from Bilb June 30, 2025 03:47
@@ -15,6 +15,7 @@ import { runScriptAndLog } from './utils/utilities';
import { getAdbFullPath } from './utils/binaries';
import { closeApp } from './utils/open_app';
import { englishStrippedStr } from '../../localizer/englishStrippedStr';
import type { TestInfo } from '@playwright/test';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usually, we want imports from the node_modules at the top of the file. Maybe check before it gets too bad to add an eslint rules and fix them all (can be a separate PR)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added perfectionist and am enforcing sort-import and sort-named-imports now.

success = true; // Exit the loop if successful
} else {
throw new Error(`longPress on conversation list: ${userName} unsuccessful`);
}
} catch (error) {
console.log(`Longpress attempt ${attempt} failed. Retrying...`);
this.log(`Longpress attempt ${attempt} failed. Retrying...`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you planned on refactoring all of the others console.* from outside of DeviceWrapper.ts to have as many with a context associated with it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the devicewrapper logs public and went through all .ts files and added in context-aware logging where it seemed appropriate.

Comment on lines +114 to 122
result.devices[0].setDeviceIdentity('alice1');
result.devices[1].setDeviceIdentity('bob1');
const seedPhrases = result.prebuilt.users.map(m => m.seedPhrase);
await linkDevices(result.devices, seedPhrases);

const alice = result.prebuilt.users[0];
const bob = result.prebuilt.users[1];
const alice1 = result.devices[0];
const bob1 = result.devices[1];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe move those just below so it's easier to read and catch mistakes

-  result.devices[0].setDeviceIdentity('alice1');
-  result.devices[1].setDeviceIdentity('bob1');
  const alice1 = result.devices[0];
  const bob1 = result.devices[1];

+ alice1.setDeviceIdentity('alice1');
+ bob1.setDeviceIdentity('bob1');

... same with the others in this file

@@ -18,6 +18,7 @@ export const linkedDevice = async (
) => {
const user = await newUser(device1, userName);
// Log in with recovery seed on device 2
device2.setDeviceIdentity(`${userName.toLowerCase()}2`);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hopefully we never linked a third device to a user using this linkedDevice function?
If we ever do, alice2 and alice3 will both be named alice2.
Maybe instead make the change to have a 4th argument to linkedDevice, being the deviceIdentity to assign?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's impossible to create alice2 and alice3 with this method currently. It first creates a new user (name1) and then logs in with that seed on device 2 (which gets set to name2). If we want to support multi-device-linking then the method needs to be completely rewritten/expanded and then a deviceNumber parameter could make sense.

Comment on lines +20 to +27
export function registerDevicesForTest(
testInfo: TestInfo,
devices: DeviceWrapper[],
platform: SupportedPlatformsType
) {
const testId = `${testInfo.testId}-${testInfo.repeatEachIndex}`;
deviceRegistry.set(testId, { devices, testInfo, platform });
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's probably safe to check if such an entry exists, and throws if that's the case.
If there is already an entry, it means a unregisterDevicesForTest wasn't called, and that means we are going to include things that have nothing to do there.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, added in a check.

Comment on lines +122 to +150
// Position remaining screenshots
const overlays = [];

// Top right (device 2)
if (screenshots[1]) {
overlays.push({
input: screenshots[1],
left: width + gap,
top: 0,
});
}

// Bottom left (device 3)
if (screenshots[2]) {
overlays.push({
input: screenshots[2],
left: 0,
top: height + gap,
});
}

// Bottom right (device 4)
if (screenshots[3]) {
overlays.push({
input: screenshots[3],
left: width + gap,
top: height + gap,
});
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be this?

const countPerRow = 2;
screenshots.slice(1).forEach((screenshot, index) => {
  overlays.push({
        input: screenshot,
        left: ((index + 1)% countPerRow )* (width + gap),
        top: (Math.floor(index/countPerRow) )(height + gap),
      });
})

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this one is the only one that needs to exist.
The others createSideBySideComposite and the single screenshot one are just specific cases.
Can you refactor it to be able to have only one of them?

Comment on lines +79 to +93
const composite = await sharp(left)
.extend({
right: width + 2, // 2px gap
background: { r: 255, g: 255, b: 255, alpha: 1 },
})
.composite([
{
input: right,
left: width + 2,
top: 0,
},
])
.png()
.toBuffer();

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this 2 a variable too outside of the function, and reuse it for createSideBySideComposite and createGridComposite

Comment on lines +75 to +76
const metadata = await sharp(left).metadata();
const width = metadata.width;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

search for metadata in all files, but this can be cleaned up with
const {width} = await sharp(left).metadata();

Comment on lines +30 to +33
export function unregisterDevicesForTest(testInfo: TestInfo) {
const testId = `${testInfo.testId}-${testInfo.repeatEachIndex}`;
deviceRegistry.delete(testId);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tested this with multiple tests running at the same time over multiple workers?
That deviceRegistry is shared over all of the runners, and I wonder if the repeat count could not be messing up when twice the same test is running at the same time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants