Skip to content

Commit 7e06003

Browse files
authored
Fix oppia#21633: Added screenshot feature for failed acceptance tests (oppia#22043)
* Acceptance Tests, Screenshot feature added * Added the custom-jest-environment file to root files config * Fixed Screenshot capturing on the retry attempt * Fixed comments * Fixed lints * Resolve comments: added failure terms * resolve comment: fixed the function name * fix path in test-constant file * Resolve comments: removed close browser object * fix CI failure
1 parent d2bdeb5 commit 7e06003

File tree

8 files changed

+108
-3
lines changed

8 files changed

+108
-3
lines changed

.github/workflows/full_stack_tests.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@ jobs:
163163
xvfb-run -a --server-args="-screen 0, 1285x1000x24"
164164
python -m scripts.run_acceptance_tests
165165
--skip-build --suite=${{ matrix.suite.name }} --prod_env
166+
- name: Upload desktop test failure screenshots as artifacts
167+
if: ${{ failure() }}
168+
uses: actions/upload-artifact@v4
169+
with:
170+
name: desktop-acceptance-test-failure-screenshot-${{steps.generate_suite_name_for_artifacts.outputs.MODIFIED_SUITE_NAME}}
171+
path: /home/runner/work/oppia/oppia_full_stack_test_failure_screenshots/acceptance
166172
- name: Run Desktop Acceptance Test ${{ matrix.suite.name }} (Attempt 2 with screen recording)
167173
if: ${{ failure() }}
168174
run: >
@@ -184,6 +190,12 @@ jobs:
184190
path: /home/runner/work/oppia/oppia/core/tests/test-modules-mappings/acceptance/${{ steps.generate_suite_name_for_artifacts.outputs.MODIFIED_SUITE_NAME }}.txt
185191
- name: Run Mobile Acceptance Test ${{ matrix.suite.name }}
186192
run: xvfb-run -a --server-args="-screen 0, 1285x1000x24" python -m scripts.run_acceptance_tests --skip-build --suite=${{ matrix.suite.name }} --prod_env --mobile
193+
- name: Upload mobile test failure screenshots as artifacts
194+
if: ${{ failure() }}
195+
uses: actions/upload-artifact@v4
196+
with:
197+
name: desktop-acceptance-test-failure-screenshot-${{steps.generate_suite_name_for_artifacts.outputs.MODIFIED_SUITE_NAME}}
198+
path: /home/runner/work/oppia/oppia_full_stack_test_failure_screenshots/acceptance
187199
- name: Run Mobile Acceptance Test ${{ matrix.suite.name }} (Attempt 2 with screen recording)
188200
if: ${{ failure() }}
189201
# We use xvfb-run to create a virtual screen to run tests without headless mode.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2025 The Oppia Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS-IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/**
16+
* @fileoverview Custom Jest environment for enhanced test failure handling.
17+
*
18+
* This custom Jest environment extends NodeEnvironment to detect test failures
19+
* in real-time and trigger actions like capturing screenshots for debugging.
20+
*/
21+
22+
const fs = require('fs');
23+
const path = require('path');
24+
const {showMessage} = require('./utilities/common/show-message');
25+
const NodeEnvironment = require('jest-environment-node');
26+
27+
const CONFIG_FILE = path.resolve(__dirname, 'jest-runtime-config.json');
28+
29+
class CustomJestEnvironment extends NodeEnvironment {
30+
async handleTestEvent(event) {
31+
if (event.name === 'test_done' && event.test.errors.length > 0) {
32+
showMessage('Test failed: Capturing screenshots...');
33+
fs.writeFileSync(
34+
CONFIG_FILE,
35+
JSON.stringify({testFailureDetected: true})
36+
);
37+
}
38+
}
39+
}
40+
41+
module.exports = CustomJestEnvironment;

core/tests/puppeteer-acceptance-tests/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ module.exports = {
2525
testMatch: ['**/?(*.)+(spec).[t]s'],
2626
transform: {'^.+\\.ts?$': 'ts-jest'},
2727
preset: 'ts-jest',
28-
testEnvironment: 'node',
28+
testEnvironment: './custom-jest-environment.js',
2929
testTimeout: 300000,
3030
bail: 0,
3131
transformIgnorePatterns: ['node_modules/(?!expect/)'],

core/tests/puppeteer-acceptance-tests/utilities/common/puppeteer-utils.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,11 @@ export class BaseUser {
7171
username: string = '';
7272
startTimeInMilliseconds: number = -1;
7373
screenRecorder!: PuppeteerScreenRecorder;
74+
static instances: BaseUser[] = []; // Track instances.
7475

75-
constructor() {}
76+
constructor() {
77+
BaseUser.instances.push(this);
78+
}
7679

7780
/**
7881
* This is a function that opens a new browser instance for the user.
@@ -202,6 +205,33 @@ export class BaseUser {
202205
return this.page;
203206
}
204207

208+
/**
209+
* This function takes the screenshot of all the instances of browser during a test failure.
210+
*/
211+
async captureScreenshotsForFailedTest(): Promise<void> {
212+
let i: number = 0;
213+
const specName = process.env.SPEC_NAME;
214+
const outputDir = testConstants.TEST_SCREENSHOT_DIR;
215+
const outputFileName = `${specName}-${new Date().toISOString()}`.replace(
216+
/[^a-z0-9.-]/gi,
217+
'_'
218+
);
219+
if (!fs.existsSync(outputDir)) {
220+
fs.mkdirSync(outputDir, {recursive: true});
221+
}
222+
for (const instance of BaseUser.instances) {
223+
if (instance.page) {
224+
await instance.page.screenshot({
225+
path: path.join(outputDir, outputFileName + `-instance-${i}.png`),
226+
});
227+
showMessage(
228+
`Screenshot captured for test failure and saved as : ${path.join(outputDir, outputFileName + `-instance-${i}.png`)}`
229+
);
230+
i = i + 1;
231+
}
232+
}
233+
}
234+
205235
/**
206236
* Checks if the application is in production mode.
207237
* @returns {Promise<boolean>} Returns true if the application is in development mode,
@@ -514,6 +544,21 @@ export class BaseUser {
514544
if (this.screenRecorder) {
515545
await this.screenRecorder.stop();
516546
}
547+
const CONFIG_FILE = path.resolve(
548+
__dirname,
549+
'../../jest-runtime-config.json'
550+
);
551+
if (
552+
fs.existsSync(CONFIG_FILE) &&
553+
!(process.env.VIDEO_RECORDING_IS_ENABLED === '1')
554+
) {
555+
const configData = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
556+
if (configData.testFailureDetected) {
557+
fs.unlinkSync(CONFIG_FILE);
558+
// Signal all BaseUser instances to take screenshots.
559+
await this.captureScreenshotsForFailedTest();
560+
}
561+
}
517562
await this.browserObject.close();
518563
}
519564

core/tests/puppeteer-acceptance-tests/utilities/common/test-constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,4 +243,7 @@ export default {
243243
TEST_VIDEO_DIR: path.resolve(
244244
'../oppia_full_stack_test_video_recordings/acceptance'
245245
),
246+
TEST_SCREENSHOT_DIR: path.resolve(
247+
'../oppia_full_stack_test_failure_screenshots/acceptance'
248+
),
246249
};

core/tests/root-files-config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"core/templates/domain/skill/MisconceptionObjectFactorySpec.ts",
6161
"core/templates/domain/exploration/AnswerGroupObjectFactorySpec.ts",
6262
"core/tests/puppeteer-acceptance-tests/jest.config.js",
63+
"core/tests/puppeteer-acceptance-tests/custom-jest-environment.js",
6364
"core/tests/puppeteer-acceptance-tests/specs/exploration-editor/manage-exploration-misconceptions.spec.ts"
6465
],
6566
"RUN_ALL_TESTS_ROOT_FILES": [

scripts/clean.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
OPPIA_TOOLS_DIR = os.path.join(CURR_DIR, '..', 'oppia_tools')
2626
FULL_STACK_TEST_VIDEO_RECORDING_DIR = os.path.join(
2727
CURR_DIR, '..', 'oppia_full_stack_test_video_recordings')
28+
FULL_STACK_TEST_SCREENSHOT_DIR = os.path.join(
29+
CURR_DIR, '..', 'oppia_full_stack_test_screenshots')
2830

2931
_PARSER = argparse.ArgumentParser(
3032
description="""
@@ -61,6 +63,7 @@ def main(args: Optional[Sequence[str]] = None) -> None:
6163

6264
delete_directory_tree(OPPIA_TOOLS_DIR)
6365
delete_directory_tree(FULL_STACK_TEST_VIDEO_RECORDING_DIR)
66+
delete_directory_tree(FULL_STACK_TEST_SCREENSHOT_DIR)
6467
delete_directory_tree('node_modules/')
6568
delete_directory_tree('third_party/')
6669
delete_directory_tree('build/')

scripts/clean_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def test_function_calls(self) -> None:
109109
'delete_file_is_called': 0
110110
}
111111
expected_check_function_calls = {
112-
'delete_directory_tree_is_called': 10,
112+
'delete_directory_tree_is_called': 11,
113113
'delete_file_is_called': 4
114114
}
115115
def mock_delete_dir(unused_path: str) -> None:

0 commit comments

Comments
 (0)